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
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=40
|
||||
max-branches=50
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=150
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=10
|
||||
max-parents=15
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=50
|
||||
|
@ -20,13 +20,12 @@ Parameters, Commands and Properties
|
||||
...................................
|
||||
|
||||
.. autoclass:: secop.params.Parameter
|
||||
.. autoclass:: secop.params.usercommand
|
||||
.. autoclass:: secop.params.Command
|
||||
.. autoclass:: secop.properties.Property
|
||||
.. autoclass:: secop.modules.Attached
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
Datatypes
|
||||
.........
|
||||
|
||||
|
@ -29,11 +29,10 @@
|
||||
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
||||
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
||||
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.params import Parameter, Command, Override, usercommand
|
||||
from secop.params import Parameter, Command
|
||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||
from secop.metaclass import Done
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.stringio import StringIO, HasIodev
|
||||
from secop.proxy import SecNode, Proxy, proxy_class
|
||||
|
@ -98,7 +98,7 @@ class DataType(HasProperties):
|
||||
def set_properties(self, **kwds):
|
||||
"""init datatype properties"""
|
||||
try:
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
self.checkProperties()
|
||||
except Exception as e:
|
||||
@ -151,11 +151,12 @@ class Stub(DataType):
|
||||
"""
|
||||
for dtcls in globals().values():
|
||||
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||
for prop in dtcls.properties.values():
|
||||
for prop in dtcls.propertyDict.values():
|
||||
stub = prop.datatype
|
||||
if isinstance(stub, cls):
|
||||
prop.datatype = globals()[stub.name](*stub.args)
|
||||
|
||||
|
||||
# SECoP types:
|
||||
|
||||
class FloatRange(DataType):
|
||||
@ -165,16 +166,14 @@ class FloatRange(DataType):
|
||||
:param maxval: (property **max**)
|
||||
:param kwds: any of the properties below
|
||||
"""
|
||||
properties = {
|
||||
'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),
|
||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
||||
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0),
|
||||
extname='absolute_resolution', default=0.0),
|
||||
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0),
|
||||
extname='relative_resolution', default=1.2e-7),
|
||||
}
|
||||
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)
|
||||
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
|
||||
extname='absolute_resolution', default=0.0)
|
||||
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
|
||||
extname='relative_resolution', default=1.2e-7)
|
||||
|
||||
def __init__(self, minval=None, maxval=None, **kwds):
|
||||
super().__init__()
|
||||
@ -204,7 +203,7 @@ class FloatRange(DataType):
|
||||
if self.min - prec <= value <= self.max + prec:
|
||||
return min(max(value, self.min), self.max)
|
||||
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
||||
(value, self.min, self.max))
|
||||
(value, self.min, self.max))
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info()
|
||||
@ -212,7 +211,7 @@ class FloatRange(DataType):
|
||||
hints['minval'] = hints.pop('min')
|
||||
if 'max' in hints:
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -247,12 +246,10 @@ class IntRange(DataType):
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', 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?
|
||||
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
|
||||
}
|
||||
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', 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?
|
||||
# unit = Property('physical unit', StringType(), extname='unit', default='')
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
super().__init__()
|
||||
@ -278,7 +275,12 @@ class IntRange(DataType):
|
||||
raise BadValueError('Can not convert %r to int' % value)
|
||||
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -315,24 +317,23 @@ class ScaledInteger(DataType):
|
||||
note: limits are for the scaled float value
|
||||
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),
|
||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
||||
'max': Property('high limit', FloatRange(), extname='max', mandatory=True),
|
||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
||||
'absolute_resolution': Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0),
|
||||
'relative_resolution': Property('relative resolution', FloatRange(0),
|
||||
extname='relative_resolution', default=1.2e-7),
|
||||
}
|
||||
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
|
||||
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
|
||||
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
|
||||
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||
absolute_resolution = Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0)
|
||||
relative_resolution = Property('relative resolution', FloatRange(0),
|
||||
extname='relative_resolution', default=1.2e-7)
|
||||
|
||||
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
||||
super().__init__()
|
||||
scale = float(scale)
|
||||
if absolute_resolution is None:
|
||||
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),
|
||||
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||
absolute_resolution=absolute_resolution,
|
||||
@ -363,8 +364,8 @@ class ScaledInteger(DataType):
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='scaled',
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + 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))
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
@ -377,15 +378,15 @@ class ScaledInteger(DataType):
|
||||
value = min(max(value, self.min), self.max)
|
||||
else:
|
||||
raise BadValueError('%g should be a float between %g and %g' %
|
||||
(value, self.min, self.max))
|
||||
(value, self.min, self.max))
|
||||
intval = int((value + self.scale * 0.5) // self.scale)
|
||||
value = float(intval * self.scale)
|
||||
return value # return 'actual' value (which is more discrete than a float)
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info(scale=float('%g' % self.scale),
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + 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))
|
||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||
|
||||
def export_value(self, value):
|
||||
@ -434,10 +435,11 @@ class EnumType(DataType):
|
||||
return EnumType(self._enum)
|
||||
|
||||
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):
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -451,7 +453,7 @@ class EnumType(DataType):
|
||||
"""return the validated (internal) value or raise"""
|
||||
try:
|
||||
return self._enum[value]
|
||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||
raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
|
||||
|
||||
def from_string(self, text):
|
||||
@ -460,6 +462,9 @@ class EnumType(DataType):
|
||||
def format_value(self, value, unit=None):
|
||||
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
||||
|
||||
def set_name(self, name):
|
||||
self._enum.name = name
|
||||
|
||||
def compatible(self, other):
|
||||
for m in self._enum.members:
|
||||
other(m)
|
||||
@ -471,12 +476,10 @@ class BLOBType(DataType):
|
||||
internally treated as bytes
|
||||
"""
|
||||
|
||||
properties = {
|
||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0),
|
||||
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True),
|
||||
}
|
||||
minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0)
|
||||
maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, minbytes=0, maxbytes=None):
|
||||
super().__init__()
|
||||
@ -538,14 +541,12 @@ class StringType(DataType):
|
||||
|
||||
for parameters see properties below
|
||||
"""
|
||||
properties = {
|
||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0),
|
||||
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED),
|
||||
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
Stub('BoolType'), extname='isUTF8', default=False),
|
||||
}
|
||||
minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0)
|
||||
maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED)
|
||||
isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
Stub('BoolType'), extname='isUTF8', default=False)
|
||||
|
||||
def __init__(self, minchars=0, maxchars=None, **kwds):
|
||||
super().__init__()
|
||||
@ -611,7 +612,8 @@ class StringType(DataType):
|
||||
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
||||
# whereas StringType is supposed to not contain '\n'
|
||||
# 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):
|
||||
def __init__(self, maxchars=None):
|
||||
if maxchars is None:
|
||||
@ -621,7 +623,7 @@ class TextType(StringType):
|
||||
def __repr__(self):
|
||||
if self.maxchars == UNLIMITED:
|
||||
return 'TextType()'
|
||||
return 'TextType(%d)' % (self.maxchars)
|
||||
return 'TextType(%d)' % self.maxchars
|
||||
|
||||
def copy(self):
|
||||
# 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
|
||||
"""
|
||||
properties = {
|
||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0),
|
||||
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True),
|
||||
}
|
||||
minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0)
|
||||
maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, members, minlen=0, maxlen=None):
|
||||
super().__init__()
|
||||
@ -714,14 +714,14 @@ class ArrayOf(DataType):
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of members"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.members.setProperty(key, value)
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||
members=self.members.export_datatype())
|
||||
members=self.members.export_datatype())
|
||||
|
||||
def __repr__(self):
|
||||
return 'ArrayOf(%s, %s, %s)' % (
|
||||
@ -806,11 +806,10 @@ class TupleOf(DataType):
|
||||
try:
|
||||
if len(value) != len(self.members):
|
||||
raise BadValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.' %
|
||||
(len(self.members)))
|
||||
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
||||
# validate elements and return as list
|
||||
return tuple(sub(elem)
|
||||
for sub, elem in zip(self.members, value))
|
||||
for sub, elem in zip(self.members, value))
|
||||
except Exception as exc:
|
||||
raise BadValueError('Can not validate:', str(exc))
|
||||
|
||||
@ -830,12 +829,12 @@ class TupleOf(DataType):
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
return '(%s)' % (', '.join([sub.format_value(elem)
|
||||
for sub, elem in zip(self.members, value)]))
|
||||
for sub, elem in zip(self.members, value)]))
|
||||
|
||||
def compatible(self, other):
|
||||
if not isinstance(other, TupleOf):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
if len(self.members) != len(other.members) :
|
||||
if len(self.members) != len(other.members):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
for a, b in zip(self.members, other.members):
|
||||
a.compatible(b)
|
||||
@ -867,15 +866,15 @@ class StructOf(DataType):
|
||||
if name not in members:
|
||||
raise ProgrammingError(
|
||||
'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):
|
||||
"""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):
|
||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||
for n, s in list(self.members.items())))
|
||||
for n, s in list(self.members.items())))
|
||||
if self.optional:
|
||||
res['optional'] = self.optional
|
||||
return res
|
||||
@ -991,8 +990,8 @@ class CommandType(DataType):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
|
||||
|
||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||
|
||||
class DataTypeType(DataType):
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is a valid datatype
|
||||
@ -1091,16 +1090,13 @@ class LimitsType(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):
|
||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||
self.enum = enum
|
||||
self._enum = enum
|
||||
|
||||
def __getattr__(self, key):
|
||||
enum = TupleOf.__getattr__(self, 'enum')
|
||||
if hasattr(enum, key):
|
||||
return getattr(enum, key)
|
||||
return TupleOf.__getattr__(self, key)
|
||||
return getattr(self._enum, key)
|
||||
|
||||
|
||||
def floatargs(kwds):
|
||||
|
@ -24,11 +24,10 @@
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.modules import Command, Parameter
|
||||
from secop.modules import Command, Parameter, HasAccessibles
|
||||
|
||||
|
||||
class Feature(metaclass=ModuleMeta):
|
||||
class Feature(HasAccessibles):
|
||||
"""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: if extra elements are implemented in the pid struct they MUST BE
|
||||
# properly described in the description of the pid Parameter
|
||||
parameters = {
|
||||
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ),
|
||||
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ),
|
||||
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True),
|
||||
'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),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), optional=True,
|
||||
), # 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),
|
||||
}
|
||||
|
||||
# parameters
|
||||
use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
|
||||
i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
|
||||
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),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), optional=True,
|
||||
) # 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)
|
||||
|
||||
|
||||
|
||||
class Has_PIDTable(HAS_PID):
|
||||
parameters = {
|
||||
'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),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange'
|
||||
}
|
||||
|
||||
# parameters
|
||||
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),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
|
||||
|
||||
|
||||
|
||||
|
||||
class HAS_Persistent(Feature):
|
||||
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
|
||||
# 'coupled' : Status.BUSY+2, # to be discussed.
|
||||
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
||||
#}
|
||||
parameters = {
|
||||
'persistent_mode': Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False),
|
||||
'is_persistent': Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True),
|
||||
'stored_value': Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True),
|
||||
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
persistent_mode = Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False)
|
||||
is_persistent = Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True)
|
||||
stored_value = Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True)
|
||||
driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' )
|
||||
|
||||
|
||||
|
||||
class HAS_Tolerance(Feature):
|
||||
# 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
|
||||
# for at least ´timewindow´ seconds.
|
||||
parameters = {
|
||||
'tolerance': Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$'),
|
||||
'timewindow': Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
tolerance = Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$')
|
||||
timewindow = Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Timeout(Feature):
|
||||
parameters = {
|
||||
'timeout': Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s'),
|
||||
}
|
||||
|
||||
# parameters
|
||||
timeout = Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s')
|
||||
|
||||
|
||||
|
||||
class HAS_Pause(Feature):
|
||||
# just a proposal, can't agree on it....
|
||||
parameters = {
|
||||
'pause': Command('pauses movement', argument=None, result=None),
|
||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
||||
argument=None, result=None),
|
||||
}
|
||||
|
||||
@Command(argument=None, result=None)
|
||||
def pause(self):
|
||||
"""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):
|
||||
parameters = {
|
||||
'ramp': Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0)),
|
||||
'use_ramp': Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True),
|
||||
'setpoint': Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
ramp =Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0))
|
||||
use_ramp = Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True)
|
||||
setpoint = Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, )
|
||||
|
||||
|
||||
|
||||
class HAS_Speed(Feature):
|
||||
parameters = {
|
||||
'speed' : Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0)),
|
||||
}
|
||||
|
||||
# parameters
|
||||
speed = Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0))
|
||||
|
||||
|
||||
|
||||
class HAS_Accel(HAS_Speed):
|
||||
parameters = {
|
||||
'accel' : Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0)),
|
||||
'decel' : Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
accel = Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0))
|
||||
decel = Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_MotorCurrents(Feature):
|
||||
parameters = {
|
||||
'movecurrent' : Parameter('Current while moving',
|
||||
datatype=FloatRange(0)),
|
||||
'idlecurrent' : Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
movecurrent = Parameter('Current while moving',
|
||||
datatype=FloatRange(0))
|
||||
idlecurrent = Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Curve(Feature):
|
||||
# proposed, not yet agreed upon!
|
||||
parameters = {
|
||||
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'),
|
||||
# XXX: tbd. (how to upload/download/select a curve?)
|
||||
}
|
||||
|
||||
# parameters
|
||||
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
|
||||
|
||||
from secop.metaclass import Done
|
||||
from secop.modules import Done
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
|
@ -67,6 +67,7 @@ SIMPLETYPES = {
|
||||
'IntRange': 'int',
|
||||
'BlobType': 'bytes',
|
||||
'StringType': 'str',
|
||||
'TextType': 'str',
|
||||
'BoolType': 'bool',
|
||||
'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):
|
||||
if what == 'class':
|
||||
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):
|
||||
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
||||
|
@ -141,7 +141,7 @@ class SequencerMixin:
|
||||
return self.read_hw_status()
|
||||
return self.Status.IDLE, ''
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
if self.seq_is_alive():
|
||||
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
|
294
secop/modules.py
294
secop/modules.py
@ -20,12 +20,11 @@
|
||||
# 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 time
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
||||
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
||||
@ -33,19 +32,147 @@ from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueErro
|
||||
SilentError, InternalError, secop_error
|
||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||
from secop.lib.enum import Enum
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Parameter, Accessible
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.poller import Poller, BasicPoller
|
||||
|
||||
|
||||
# XXX: connect with 'protocol'-Modules.
|
||||
# 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?)
|
||||
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
||||
|
||||
|
||||
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
|
||||
|
||||
all SECoP modules derive from this.
|
||||
@ -58,7 +185,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
Notes:
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
# with data from a 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,
|
||||
# datatype is fixed!
|
||||
properties = {
|
||||
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False),
|
||||
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'),
|
||||
'description': Property('Description of the module', TextType(), extname='description', mandatory=True),
|
||||
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
||||
default=('',0), extname='meaning'),
|
||||
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility'),
|
||||
'implementation': Property('Internal name of the implementation class of the module', StringType(),
|
||||
extname='implementation'),
|
||||
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes'),
|
||||
# what else?
|
||||
}
|
||||
export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
|
||||
group = Property('optional group the module belongs to', StringType(), default='', extname='group')
|
||||
description = Property('description of the module', TextType(), extname='description', mandatory=True)
|
||||
meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
|
||||
default=('', 0), extname='meaning')
|
||||
visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility')
|
||||
implementation = Property('internal name of the implementation class of the module', StringType(),
|
||||
extname='implementation')
|
||||
interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes')
|
||||
|
||||
# properties, parameters and commands are auto-merged upon subclassing
|
||||
parameters = {}
|
||||
@ -113,14 +236,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
# handle module properties
|
||||
# 1) make local copies of properties
|
||||
super(Module, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
# 2) check and apply properties specified in cfgdict
|
||||
# specified as '.<propertyname> = <propertyvalue>'
|
||||
# (this is for legacy config files only)
|
||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||
if k[0] == '.':
|
||||
if k[1:] in self.__class__.properties:
|
||||
if k[1:] in self.propertyDict:
|
||||
self.setProperty(k[1:], cfgdict.pop(k))
|
||||
else:
|
||||
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
|
||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||
for k in self.__class__.properties:
|
||||
for k in self.propertyDict:
|
||||
if k in cfgdict:
|
||||
self.setProperty(k, cfgdict.pop(k))
|
||||
|
||||
# 4) set automatic properties
|
||||
mycls = self.__class__
|
||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||
self.properties['implementation'] = myclassname
|
||||
self.implementation = myclassname
|
||||
# list of all 'secop' modules
|
||||
self.properties['interface_classes'] = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# self.interface_classes = [
|
||||
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# list of only the 'highest' secop module class
|
||||
self.properties['interface_classes'] = [[
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
||||
self.interface_classes = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
|
||||
|
||||
# handle Features
|
||||
# XXX: todo
|
||||
@ -150,7 +273,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# 1) make local copies of parameter objects
|
||||
# they need to be individual per instance since we use them also
|
||||
# to cache the current value + qualifiers...
|
||||
accessibles = OrderedDict()
|
||||
accessibles = {}
|
||||
# conversion from exported names to internal attribute names
|
||||
accessiblename2attr = {}
|
||||
for aname, aobj in self.accessibles.items():
|
||||
@ -159,31 +282,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if isinstance(aobj, Parameter):
|
||||
# fix default properties poll and needscfg
|
||||
if aobj.poll is None:
|
||||
aobj.properties['poll'] = bool(aobj.handler)
|
||||
aobj.poll = bool(aobj.handler)
|
||||
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
|
||||
aobj.properties['export'] = False
|
||||
aobj.export = False
|
||||
if aobj.export:
|
||||
if aobj.export is True:
|
||||
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
||||
if predefined_obj:
|
||||
if isinstance(aobj, predefined_obj):
|
||||
aobj.setProperty('export', aname)
|
||||
aobj.export = aname
|
||||
else:
|
||||
raise ProgrammingError("can not use '%s' as name of a %s" %
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.setProperty('export', '_' + aname)
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.export = '_' + aname
|
||||
accessiblename2attr[aobj.export] = aname
|
||||
accessibles[aname] = aobj
|
||||
# do not re-use self.accessibles as this is the same for all instances
|
||||
self.accessibles = accessibles
|
||||
self.accessiblename2attr = accessiblename2attr
|
||||
# provide properties to 'filter' out the parameters/commands
|
||||
self.parameters = 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.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
|
||||
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
|
||||
|
||||
# 2) check and apply parameter_properties
|
||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||
@ -200,6 +323,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
else:
|
||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||
(self.name, paramname, propname))
|
||||
else:
|
||||
raise ConfigError('Module %s has no Parameter %r!' %
|
||||
(self.name, paramname))
|
||||
|
||||
# 3) check config for problems:
|
||||
# only accept remaining config items specified in parameters
|
||||
@ -209,7 +335,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
'Module %s:config Parameter %r '
|
||||
'not understood! (use one of %s)' %
|
||||
(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
|
||||
# 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)
|
||||
self.valueCallbacks[pname].append(cb)
|
||||
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""helper function for treating substates of BUSY correctly"""
|
||||
# defined even for non drivable (used for dynamic polling)
|
||||
@ -403,31 +528,22 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
"""basic readable Module"""
|
||||
"""basic readable module"""
|
||||
# pylint: disable=invalid-name
|
||||
Status = Enum('Status',
|
||||
IDLE = 100,
|
||||
WARN = 200,
|
||||
UNSTABLE = 270,
|
||||
ERROR = 400,
|
||||
DISABLED = 0,
|
||||
UNKNOWN = 401,
|
||||
) #: status codes
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
datatype=FloatRange(),
|
||||
poll=True,
|
||||
),
|
||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
||||
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,
|
||||
),
|
||||
}
|
||||
IDLE=100,
|
||||
WARN=200,
|
||||
UNSTABLE=270,
|
||||
ERROR=400,
|
||||
DISABLED=0,
|
||||
UNKNOWN=401,
|
||||
) #: status codes
|
||||
|
||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||
default=(Status.IDLE, ''), poll=True)
|
||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||
default=5, readonly=False)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""start basic polling thread"""
|
||||
@ -476,11 +592,9 @@ class Readable(Module):
|
||||
|
||||
class Writable(Readable):
|
||||
"""basic writable module"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
),
|
||||
}
|
||||
|
||||
target = Parameter('target value of the module',
|
||||
default=0, readonly=False, datatype=FloatRange())
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
@ -488,17 +602,7 @@ class Drivable(Writable):
|
||||
|
||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||
|
||||
commands = {
|
||||
'stop': Command(
|
||||
'cease driving, go to IDLE state',
|
||||
argument=None,
|
||||
result=None
|
||||
),
|
||||
}
|
||||
|
||||
overrides = {
|
||||
'status': Override(datatype=StatusType(Status)),
|
||||
}
|
||||
status = Parameter(datatype=StatusType(Status)) # override Readable.status
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""check for busy, treating substates correctly
|
||||
@ -532,23 +636,16 @@ class Drivable(Writable):
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
def do_stop(self):
|
||||
"""default implementation of the stop command
|
||||
|
||||
by default does nothing."""
|
||||
@Command(None, result=None)
|
||||
def stop(self):
|
||||
"""cease driving, go to IDLE state"""
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
"""basic abstract communication module"""
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
argument=StringType(),
|
||||
result=StringType()
|
||||
),
|
||||
}
|
||||
|
||||
def do_communicate(self, command):
|
||||
@Command(StringType(), result=StringType())
|
||||
def communicate(self, command):
|
||||
"""communicate command
|
||||
|
||||
: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
|
||||
def __init__(self, attrname=None):
|
||||
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):
|
||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||
|
593
secop/params.py
593
secop/params.py
@ -24,62 +24,69 @@
|
||||
|
||||
|
||||
import inspect
|
||||
import itertools
|
||||
from collections import OrderedDict
|
||||
|
||||
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.properties import HasProperties, Property
|
||||
|
||||
|
||||
object_counter = itertools.count(1)
|
||||
UNSET = object() # an argument not given, not even None
|
||||
|
||||
|
||||
class Accessible(HasProperties):
|
||||
"""base class for Parameter and Command"""
|
||||
|
||||
properties = {}
|
||||
kwds = None # is a dict if it might be used as Override
|
||||
|
||||
def __init__(self, ctr, **kwds):
|
||||
self.ctr = ctr or next(object_counter)
|
||||
super(Accessible, self).__init__()
|
||||
# do not use self.properties.update here, as no invalid values should be
|
||||
def __init__(self, **kwds):
|
||||
super().__init__()
|
||||
self.init(kwds)
|
||||
|
||||
def init(self, kwds):
|
||||
# do not use self.propertyValues.update here, as no invalid values should be
|
||||
# assigned to properties, even not before checkProperties
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
props = []
|
||||
for k, prop in sorted(self.__class__.properties.items()):
|
||||
v = self.properties.get(k, prop.default)
|
||||
if v != prop.default:
|
||||
props.append('%s=%r' % (k, v))
|
||||
return '%s(%s, ctr=%d)' % (self.__class__.__name__, ', '.join(props), self.ctr)
|
||||
def inherit(self, cls, owner):
|
||||
for base in owner.__bases__:
|
||||
if hasattr(base, self.name):
|
||||
aobj = getattr(base, 'accessibles', {}).get(self.name)
|
||||
if aobj:
|
||||
if not isinstance(aobj, cls):
|
||||
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):
|
||||
return self.properties
|
||||
return self.propertyValues
|
||||
|
||||
def override(self, from_object=None, **kwds):
|
||||
"""return a copy of ourselfs, modified by <other>"""
|
||||
props = dict(self.properties, ctr=self.ctr)
|
||||
if from_object:
|
||||
props.update(from_object.kwds)
|
||||
props.update(kwds)
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
return type(self)(inherit=False, internally_called=True, **props)
|
||||
def override(self, value=UNSET, **kwds):
|
||||
"""return a copy, overridden by a bare attribute
|
||||
|
||||
and/or some properties"""
|
||||
raise NotImplementedError
|
||||
|
||||
def copy(self):
|
||||
"""return a copy of ourselfs"""
|
||||
props = dict(self.properties, ctr=self.ctr)
|
||||
# deep copy, as datatype might be altered from config
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
return type(self)(inherit=False, internally_called=True, **props)
|
||||
"""return a (deep) copy of ourselfs"""
|
||||
raise NotImplementedError
|
||||
|
||||
def for_export(self):
|
||||
"""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):
|
||||
@ -87,65 +94,78 @@ class Parameter(Accessible):
|
||||
|
||||
:param description: description
|
||||
:param datatype: the datatype
|
||||
: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 inherit: whether properties not given should be inherited
|
||||
:param kwds: optional properties
|
||||
:param ctr: (for internal use only)
|
||||
:param internally_used: (for internal use only)
|
||||
"""
|
||||
# storage for Parameter settings + value + qualifiers
|
||||
|
||||
properties = {
|
||||
'description': Property('mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True),
|
||||
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True),
|
||||
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', default=True),
|
||||
'group': Property('optional parameter group this parameter belongs to', StringType(),
|
||||
extname='group', default=''),
|
||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', default=1),
|
||||
'constant': Property('optional constant value for constant parameters', ValueType(),
|
||||
extname='constant', default=None, mandatory=False),
|
||||
'default': Property('[internal] default (startup) value of this parameter '
|
||||
'if it can not be read from the hardware',
|
||||
ValueType(), export=False, default=None, mandatory=False),
|
||||
'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),
|
||||
'poll': Property('''
|
||||
[internal] polling indicator
|
||||
|
||||
may be:
|
||||
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''',
|
||||
NoneOr(IntRange()), export=False, default=None),
|
||||
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
|
||||
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
|
||||
settable=False, default=False),
|
||||
'handler': Property('[internal] overload the standard read and write functions',
|
||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
||||
'initwrite': Property('[internal] write this parameter on initialization'
|
||||
' (default None: write if given in config)',
|
||||
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
|
||||
}
|
||||
description = Property(
|
||||
'mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True)
|
||||
datatype = Property(
|
||||
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True)
|
||||
readonly = Property(
|
||||
'not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', default=True, export='always')
|
||||
group = Property(
|
||||
'optional parameter group this parameter belongs to', StringType(),
|
||||
extname='group', default='')
|
||||
visibility = Property(
|
||||
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', default=1)
|
||||
constant = Property(
|
||||
'optional constant value for constant parameters', ValueType(),
|
||||
extname='constant', default=None)
|
||||
default = Property(
|
||||
'''[internal] default (startup) value of this parameter
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, *,
|
||||
reorder=False, ctr=None, internally_called=False, **kwds):
|
||||
if it can not be read from the hardware''', ValueType(),
|
||||
export=False, default=None)
|
||||
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)
|
||||
poll = Property(
|
||||
'''[internal] polling indicator
|
||||
|
||||
may be:
|
||||
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''', NoneOr(IntRange()),
|
||||
export=False, default=None)
|
||||
needscfg = Property(
|
||||
'[internal] needs value in config', NoneOr(BoolType()),
|
||||
export=False, default=None)
|
||||
optional = Property(
|
||||
'[internal] is this parameter optional?', BoolType(),
|
||||
export=False, settable=False, default=False)
|
||||
handler = Property(
|
||||
'[internal] overload the standard read and write functions', ValueType(),
|
||||
export=False, default=None, settable=False)
|
||||
initwrite = Property(
|
||||
'''[internal] write this parameter on initialization
|
||||
|
||||
default None: write if given in config''', NoneOr(BoolType()),
|
||||
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 not isinstance(datatype, DataType):
|
||||
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
||||
@ -154,57 +174,92 @@ class Parameter(Accessible):
|
||||
else:
|
||||
raise ProgrammingError(
|
||||
'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 not internally_called:
|
||||
description = inspect.cleandoc(description)
|
||||
kwds['description'] = description
|
||||
self.description = inspect.cleandoc(description)
|
||||
|
||||
unit = kwds.pop('unit', None)
|
||||
if unit is not None and datatype: # for legacy code only
|
||||
datatype.setProperty('unit', unit)
|
||||
# save for __set_name__
|
||||
self._inherit = inherit
|
||||
self._unit = unit # for legacy code only
|
||||
self._constant = constant
|
||||
|
||||
constant = kwds.get('constant')
|
||||
if constant is not None:
|
||||
constant = datatype(constant)
|
||||
def __get__(self, instance, owner):
|
||||
# not used yet
|
||||
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
|
||||
# serialised version of the constant, or unset
|
||||
kwds['constant'] = datatype.export_value(constant)
|
||||
kwds['readonly'] = True
|
||||
if internally_called: # fixes in case datatype has changed
|
||||
default = kwds.get('default')
|
||||
if default is not None:
|
||||
try:
|
||||
datatype(default)
|
||||
except BadValueError:
|
||||
# clear default, if it does not match datatype
|
||||
kwds['default'] = None
|
||||
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
|
||||
self.constant = self.datatype.export_value(constant)
|
||||
self.readonly = True
|
||||
|
||||
# internal caching: value and timestamp of last change...
|
||||
self.value = self.default
|
||||
self.timestamp = 0
|
||||
self.readerror = None # if not None, indicates that last read was not successful
|
||||
if 'default' in self.propertyValues:
|
||||
# fixes in case datatype has changed
|
||||
try:
|
||||
self.datatype(self.default)
|
||||
except BadValueError:
|
||||
# clear default, if it does not match datatype
|
||||
self.propertyValues.pop('default')
|
||||
|
||||
if self.export is True:
|
||||
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||
self.export = name
|
||||
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):
|
||||
return self.datatype.export_value(self.value)
|
||||
|
||||
def for_export(self):
|
||||
return dict(self.exportProperties(), readonly=self.readonly)
|
||||
|
||||
def getProperties(self):
|
||||
"""get also properties of datatype"""
|
||||
superProp = super().getProperties().copy()
|
||||
superProp.update(self.datatype.getProperties())
|
||||
return superProp
|
||||
super_prop = super().getProperties().copy()
|
||||
super_prop.update(self.datatype.getProperties())
|
||||
return super_prop
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of datatype"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.datatype.setProperty(key, value)
|
||||
@ -213,208 +268,168 @@ class Parameter(Accessible):
|
||||
super().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):
|
||||
# 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
|
||||
|
||||
:param argument: the datatype of the argument or None
|
||||
:param result: the datatype of the result or None
|
||||
: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 inherit: whether properties not given should be inherited
|
||||
: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
|
||||
|
||||
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):
|
||||
# normal case
|
||||
self.func = None
|
||||
if argument is False and result:
|
||||
argument = None
|
||||
if argument is not False:
|
||||
if isinstance(argument, (tuple, list)):
|
||||
# goodie: allow declaring multiple arguments as a tuple
|
||||
# TODO: check that calling works properly
|
||||
# goodie: treat as TupleOf
|
||||
argument = TupleOf(*argument)
|
||||
kwds['argument'] = argument
|
||||
kwds['result'] = result
|
||||
self.kwds = kwds
|
||||
self.argument = argument
|
||||
self.result = result
|
||||
else:
|
||||
# goodie: allow @usercommand instead of @usercommand()
|
||||
# goodie: allow @Command instead of @Command()
|
||||
self.func = argument # this is the wrapped method!
|
||||
if argument.__doc__ is not None:
|
||||
kwds['description'] = argument.__doc__
|
||||
if argument.__doc__:
|
||||
self.description = inspect.cleandoc(argument.__doc__)
|
||||
self.name = self.func.__name__
|
||||
super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds)
|
||||
|
||||
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
|
||||
self._inherit = inherit # save for __set_name__
|
||||
|
||||
def __set_name__(self, owner, 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):
|
||||
if obj is None:
|
||||
return self
|
||||
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)
|
||||
|
||||
def __call__(self, fun):
|
||||
description = self.kwds.get('description') or fun.__doc__
|
||||
self.properties['description'] = self.kwds['description'] = description
|
||||
self.name = fun.__name__
|
||||
self.func = fun
|
||||
def __call__(self, func):
|
||||
if 'description' not in self.propertyValues and func.__doc__:
|
||||
self.description = inspect.cleandoc(func.__doc__)
|
||||
self.func = func
|
||||
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
|
||||
PREDEFINED_ACCESSIBLES = dict(
|
||||
value = Parameter,
|
||||
status = Parameter,
|
||||
target = Parameter,
|
||||
pollinterval = Parameter,
|
||||
ramp = Parameter,
|
||||
user_ramp = Parameter,
|
||||
setpoint = Parameter,
|
||||
time_to_target = Parameter,
|
||||
unit = Parameter, # reserved name
|
||||
loglevel = Parameter, # reserved name
|
||||
mode = Parameter, # reserved name
|
||||
stop = Command,
|
||||
reset = Command,
|
||||
go = Command,
|
||||
abort = Command,
|
||||
shutdown = Command,
|
||||
communicate = Command,
|
||||
value=Parameter,
|
||||
status=Parameter,
|
||||
target=Parameter,
|
||||
pollinterval=Parameter,
|
||||
ramp=Parameter,
|
||||
user_ramp=Parameter,
|
||||
setpoint=Parameter,
|
||||
time_to_target=Parameter,
|
||||
unit=Parameter, # reserved name
|
||||
loglevel=Parameter, # reserved name
|
||||
mode=Parameter, # reserved name
|
||||
stop=Command,
|
||||
reset=Command,
|
||||
go=Command,
|
||||
abort=Command,
|
||||
shutdown=Command,
|
||||
communicate=Command,
|
||||
)
|
||||
|
@ -23,27 +23,44 @@
|
||||
"""Define validated data types."""
|
||||
|
||||
|
||||
import sys
|
||||
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):
|
||||
properties = {}
|
||||
# allow to declare properties directly as class attribute
|
||||
# all these attributes are removed
|
||||
for k, v in attrs.items():
|
||||
if isinstance(v, tuple) and v and isinstance(v[0], itemcls):
|
||||
# this might happen when migrating from old to new style
|
||||
raise ProgrammingError('declared %r with trailing comma' % k)
|
||||
if isinstance(v, itemcls):
|
||||
properties[k] = v
|
||||
if remove:
|
||||
for k in properties:
|
||||
attrs.pop(k)
|
||||
properties.update(attrs.get(dictname, {}))
|
||||
attrs[dictname] = properties
|
||||
class HasDescriptorMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if sys.version_info < (3, 6):
|
||||
# support older python versions
|
||||
for key, attr in attrs.items():
|
||||
if hasattr(attr, '__set_name__'):
|
||||
attr.__set_name__(newtype, key)
|
||||
newtype.__init_subclass__()
|
||||
return newtype
|
||||
|
||||
|
||||
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||
@classmethod
|
||||
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'
|
||||
@ -56,7 +73,8 @@ class Property:
|
||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||
when they match the default
|
||||
: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
|
||||
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
|
||||
@ -64,148 +82,134 @@ class Property:
|
||||
|
||||
# note: this is intended to be used on base classes.
|
||||
# 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):
|
||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||
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.extname = extname
|
||||
self.export = export or bool(extname)
|
||||
if mandatory is None:
|
||||
mandatory = default is None
|
||||
mandatory = default is UNSET
|
||||
self.mandatory = mandatory
|
||||
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):
|
||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
||||
self.description, self.datatype, self.default, self.extname, self.export,
|
||||
self.mandatory, self.settable)
|
||||
extras = ['default=%s' % repr(self.default)]
|
||||
if self.export:
|
||||
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):
|
||||
"""a collection of `Property` objects
|
||||
|
||||
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 = {}
|
||||
class HasProperties(HasDescriptors):
|
||||
propertyValues = None
|
||||
|
||||
def __init__(self):
|
||||
super(HasProperties, self).__init__()
|
||||
self.initProperties()
|
||||
|
||||
def initProperties(self):
|
||||
# store property values in the instance, keep descriptors on the class
|
||||
self.properties = {}
|
||||
# pre-init with properties default value (if any)
|
||||
for pn, po in self.__class__.properties.items():
|
||||
value = getattr(self, '_initProp_' + pn, self)
|
||||
if value is not self: # property value was given as attribute
|
||||
self.properties[pn] = value
|
||||
elif not po.mandatory:
|
||||
self.properties[pn] = po.default
|
||||
self.propertyValues = {}
|
||||
# pre-init
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.value is not UNSET:
|
||||
self.setProperty(pn, po.value)
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
"""validates properties and checks for min... <= max..."""
|
||||
for pn, po in self.__class__.properties.items():
|
||||
if po.export and po.mandatory:
|
||||
if pn not in self.properties:
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.mandatory:
|
||||
if pn not in self.propertyDict:
|
||||
name = getattr(self, 'name', self.__class__.__name__)
|
||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||
# apply validator (which may complain further)
|
||||
self.properties[pn] = po.datatype(self.properties[pn])
|
||||
for pn, po in self.__class__.properties.items():
|
||||
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||
for pn, po in self.propertyDict.items():
|
||||
if pn.startswith('min'):
|
||||
maxname = 'max' + pn[3:]
|
||||
minval = self.properties[pn]
|
||||
maxval = self.properties.get(maxname, minval)
|
||||
minval = self.propertyValues.get(pn, po.default)
|
||||
maxval = self.propertyValues.get(maxname, minval)
|
||||
if minval > maxval:
|
||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||
|
||||
|
||||
def getProperties(self):
|
||||
return self.__class__.properties
|
||||
return self.propertyDict
|
||||
|
||||
def exportProperties(self):
|
||||
# export properties which have
|
||||
# export=True and
|
||||
# mandatory=True or non_default=True
|
||||
res = {}
|
||||
for pn, po in self.__class__.properties.items():
|
||||
val = self.properties.get(pn, None)
|
||||
if po.export and (po.mandatory or val != po.default):
|
||||
for pn, po in self.propertyDict.items():
|
||||
val = self.propertyValues.get(pn, po.default)
|
||||
if po.export and (po.export == 'always' or val != po.default):
|
||||
try:
|
||||
val = po.datatype.export_value(val)
|
||||
except AttributeError:
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
res[po.extname] = val
|
||||
return res
|
||||
|
||||
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 time import time as currenttime
|
||||
|
||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
||||
from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
||||
from secop.params import Parameter
|
||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
@ -53,10 +53,10 @@ from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
def make_update(modulename, pobj):
|
||||
if pobj.readerror:
|
||||
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||
# error-report !
|
||||
[pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)])
|
||||
# error-report !
|
||||
[pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)])
|
||||
return (EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
@ -109,7 +109,7 @@ class Dispatcher:
|
||||
self._subscriptions.setdefault(eventname, set()).add(conn)
|
||||
|
||||
def unsubscribe(self, conn, eventname):
|
||||
if not ':' in eventname:
|
||||
if ':' not in eventname:
|
||||
# also remove 'more specific' subscriptions
|
||||
for k, v in self._subscriptions.items():
|
||||
if k.startswith('%s:' % eventname):
|
||||
@ -177,7 +177,7 @@ class Dispatcher:
|
||||
result = {'modules': OrderedDict()}
|
||||
for modulename in self._export:
|
||||
module = self.get_module(modulename)
|
||||
if not module.properties.get('export', False):
|
||||
if not module.export:
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||
@ -186,7 +186,7 @@ class Dispatcher:
|
||||
result['modules'][modulename] = mod_desc
|
||||
result['equipment_id'] = self.equipment_id
|
||||
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
||||
result['version'] = '2019.08'
|
||||
result['version'] = '2021.02'
|
||||
result.update(self.nodeprops)
|
||||
return result
|
||||
|
||||
@ -195,40 +195,24 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
||||
if cmdname is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
||||
cmdspec = moduleobj.commands[cmdname]
|
||||
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)
|
||||
cname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
cobj = moduleobj.commands.get(cname)
|
||||
if cobj is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||
|
||||
# now call func
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
func = getattr(moduleobj, 'do_' + cmdname)
|
||||
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())
|
||||
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||
|
||||
def _setParameterValue(self, modulename, exportedname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||
% (modulename, pname))
|
||||
@ -252,10 +236,10 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
# really needed? we could just construct a readreply instead....
|
||||
# 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)))
|
||||
|
||||
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)
|
||||
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
||||
|
||||
def handle_ping(self, conn, specifier, data):
|
||||
if 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):
|
||||
if data:
|
||||
|
@ -28,14 +28,11 @@ from secop.properties import Property
|
||||
from secop.stringio import HasIodev
|
||||
from secop.lib import get_class
|
||||
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):
|
||||
properties = {
|
||||
'module':
|
||||
Property('remote module name', datatype=StringType(), default=''),
|
||||
}
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
|
||||
pollerClass = None
|
||||
_consistency_check_done = False
|
||||
@ -55,7 +52,7 @@ class ProxyModule(HasIodev, Module):
|
||||
|
||||
def initModule(self):
|
||||
if not self.module:
|
||||
self.properties['module'] = self.name
|
||||
self.module = self.name
|
||||
self._secnode = self._iodev.secnode
|
||||
self._secnode.register_callback(self.module, self.updateEvent,
|
||||
self.descriptiveDataChange, self.nodeStateChange)
|
||||
@ -103,9 +100,9 @@ class ProxyModule(HasIodev, Module):
|
||||
dt = props['datatype']
|
||||
try:
|
||||
cobj.datatype.compatible(dt)
|
||||
except Exception:
|
||||
except BadValueError:
|
||||
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?
|
||||
# we might raise an exception, but this would lead to a reconnection,
|
||||
# which might not help.
|
||||
@ -141,14 +138,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
||||
|
||||
|
||||
class SecNode(Module):
|
||||
properties = {
|
||||
'uri':
|
||||
Property('uri of a SEC node', datatype=StringType()),
|
||||
}
|
||||
commands = {
|
||||
'request':
|
||||
Command('send a request', argument=StringType(), result=StringType())
|
||||
}
|
||||
uri = Property('uri of a SEC node', datatype=StringType())
|
||||
|
||||
def earlyInit(self):
|
||||
self.secnode = SecopClient(self.uri, self.log)
|
||||
@ -156,8 +146,9 @@ class SecNode(Module):
|
||||
def startModule(self, started_callback):
|
||||
self.secnode.spawn_connect(started_callback)
|
||||
|
||||
def do_request(self, msg):
|
||||
"""for test purposes"""
|
||||
@Command(StringType(), result=StringType())
|
||||
def request(self, msg):
|
||||
"""send a request, for debugging purposes"""
|
||||
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
||||
return encode_msg_frame(*reply).decode('utf-8')
|
||||
|
||||
@ -184,17 +175,12 @@ def proxy_class(remote_class, name=None):
|
||||
else:
|
||||
raise ConfigError('%r is no SECoP module class' % remote_class)
|
||||
|
||||
parameters = {}
|
||||
commands = {}
|
||||
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
||||
attrs = rcls.propertyDict.copy()
|
||||
|
||||
for aname, aobj in rcls.accessibles.items():
|
||||
if isinstance(aobj, Parameter):
|
||||
pobj = aobj.copy()
|
||||
parameters[aname] = pobj
|
||||
pobj.properties['poll'] = False
|
||||
pobj.properties['handler'] = None
|
||||
pobj.properties['needscfg'] = False
|
||||
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||
attrs[aname] = pobj
|
||||
|
||||
def rfunc(self, pname=aname):
|
||||
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
||||
@ -216,12 +202,11 @@ def proxy_class(remote_class, name=None):
|
||||
|
||||
elif isinstance(aobj, Command):
|
||||
cobj = aobj.copy()
|
||||
commands[aname] = cobj
|
||||
|
||||
def cfunc(self, arg=None, cname=aname):
|
||||
return self._secnode.execCommand(self.name, cname, arg)
|
||||
|
||||
attrs['do_' + aname] = cfunc
|
||||
attrs[aname] = cobj(cfunc)
|
||||
|
||||
else:
|
||||
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
|
||||
for modname, modobj in self.modules.items():
|
||||
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:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
@ -236,10 +236,10 @@ class Server:
|
||||
|
||||
# handle attached modules
|
||||
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):
|
||||
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
|
||||
for modname, modobj in self.modules.items():
|
||||
modobj.initModule()
|
||||
|
@ -22,6 +22,8 @@
|
||||
"""Define Simulation classes"""
|
||||
|
||||
|
||||
# TODO: rework after syntax change!
|
||||
|
||||
import random
|
||||
from time import sleep
|
||||
|
||||
|
@ -27,11 +27,10 @@ import time
|
||||
import threading
|
||||
import re
|
||||
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.errors import CommunicationFailedError, CommunicationSilentError
|
||||
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
|
||||
from secop.poller import REGULAR
|
||||
from secop.metaclass import Done
|
||||
|
||||
|
||||
class StringIO(Communicator):
|
||||
@ -39,38 +38,22 @@ class StringIO(Communicator):
|
||||
|
||||
self healing is assured by polling the parameter 'is_connected'
|
||||
"""
|
||||
properties = {
|
||||
'uri':
|
||||
Property('hostname:portnumber', datatype=StringType()),
|
||||
'end_of_line':
|
||||
Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True),
|
||||
'encoding':
|
||||
Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True),
|
||||
'identification':
|
||||
Property('''
|
||||
identification
|
||||
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
||||
}
|
||||
parameters = {
|
||||
'timeout':
|
||||
Parameter('timeout', datatype=FloatRange(0), default=2),
|
||||
'wait_before':
|
||||
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()))
|
||||
}
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
end_of_line = Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True)
|
||||
encoding = Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True)
|
||||
identification = Property('''
|
||||
identification
|
||||
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
||||
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = 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)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
|
||||
@ -105,11 +88,12 @@ class StringIO(Communicator):
|
||||
self._conn = AsynConn(uri, self._eol_read)
|
||||
self.is_connected = True
|
||||
for command, regexp in self.identification:
|
||||
reply = self.do_communicate(command)
|
||||
reply = self.communicate(command)
|
||||
if not re.match(regexp, reply):
|
||||
self.closeConnection()
|
||||
raise CommunicationFailedError('bad response: %s does not match %s' %
|
||||
(reply, regexp))
|
||||
|
||||
def closeConnection(self):
|
||||
"""close connection
|
||||
|
||||
@ -125,7 +109,7 @@ class StringIO(Communicator):
|
||||
self.is_connected is changed only by self.connectStart or self.closeConnection
|
||||
"""
|
||||
if self.is_connected:
|
||||
return Done # no need for intermediate updates
|
||||
return Done # no need for intermediate updates
|
||||
try:
|
||||
self.connectStart()
|
||||
if self._last_error:
|
||||
@ -170,7 +154,7 @@ class StringIO(Communicator):
|
||||
if removeme:
|
||||
self._reconnectCallbacks.pop(key)
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
"""send a command and receive a reply
|
||||
|
||||
using end_of_line, encoding and self._lock
|
||||
@ -179,6 +163,8 @@ class StringIO(Communicator):
|
||||
"""
|
||||
if not self.is_connected:
|
||||
self.read_is_connected() # try to reconnect
|
||||
if not self._conn:
|
||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||
try:
|
||||
with self._lock:
|
||||
# read garbage and wait before send
|
||||
@ -210,11 +196,13 @@ class StringIO(Communicator):
|
||||
self.log.error(self._last_error)
|
||||
raise
|
||||
|
||||
def do_multicomm(self, commands):
|
||||
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
def multicomm(self, commands):
|
||||
"""communicate multiple request/replies in one row"""
|
||||
replies = []
|
||||
with self._lock:
|
||||
for cmd in commands:
|
||||
replies.append(self.do_communicate(cmd))
|
||||
replies.append(self.communicate(cmd))
|
||||
return replies
|
||||
|
||||
|
||||
@ -223,17 +211,15 @@ class HasIodev(Module):
|
||||
|
||||
not only StringIO !
|
||||
"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
'uri': Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default=''),
|
||||
}
|
||||
iodev = Attached()
|
||||
uri = Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default='')
|
||||
|
||||
iodevDict = {}
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
iodev = opts.get('iodev')
|
||||
super().__init__(name, logger, opts, srv)
|
||||
Module.__init__(self, name, logger, opts, srv)
|
||||
if self.uri:
|
||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||
'export': False}
|
||||
@ -243,7 +229,9 @@ class HasIodev(Module):
|
||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||
srv.modules[ioname] = iodev
|
||||
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):
|
||||
try:
|
||||
@ -254,4 +242,4 @@ class HasIodev(Module):
|
||||
super().initModule()
|
||||
|
||||
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.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)
|
||||
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):
|
||||
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):
|
||||
@ -49,93 +47,88 @@ class Cryostat(CryoBase):
|
||||
- thermal transfer between regulation and samplen
|
||||
"""
|
||||
|
||||
parameters = dict(
|
||||
jitter=Parameter("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
default=0.1, readonly=False, export=False,
|
||||
),
|
||||
T_start=Parameter("starting temperature for simulation",
|
||||
datatype=FloatRange(0), default=10,
|
||||
export=False,
|
||||
),
|
||||
looptime=Parameter("timestep for simulation",
|
||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||
readonly=False, export=False,
|
||||
jitter = Parameter("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
default=0.1, readonly=False, export=False,
|
||||
),
|
||||
ramp=Parameter("ramping speed of the setpoint",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False,
|
||||
),
|
||||
setpoint=Parameter("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K',
|
||||
),
|
||||
maxpower=Parameter("Maximum heater power",
|
||||
datatype=FloatRange(0), default=1, unit="W",
|
||||
readonly=False,
|
||||
group='heater_settings',
|
||||
),
|
||||
heater=Parameter("current heater setting",
|
||||
datatype=FloatRange(0, 100), default=0, unit="%",
|
||||
group='heater_settings',
|
||||
),
|
||||
heaterpower=Parameter("current heater power",
|
||||
datatype=FloatRange(0), default=0, unit="W",
|
||||
group='heater_settings',
|
||||
),
|
||||
target=Override("target temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
T_start = Parameter("starting temperature for simulation",
|
||||
datatype=FloatRange(0), default=10,
|
||||
export=False,
|
||||
),
|
||||
looptime = Parameter("timestep for simulation",
|
||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||
readonly=False, export=False,
|
||||
),
|
||||
ramp = Parameter("ramping speed of the setpoint",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False,
|
||||
),
|
||||
value=Override("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
test='TEST',
|
||||
setpoint = Parameter("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K',
|
||||
),
|
||||
maxpower = Parameter("Maximum heater power",
|
||||
datatype=FloatRange(0), default=1, unit="W",
|
||||
readonly=False,
|
||||
group='heater_settings',
|
||||
),
|
||||
heater = Parameter("current heater setting",
|
||||
datatype=FloatRange(0, 100), default=0, unit="%",
|
||||
group='heater_settings',
|
||||
),
|
||||
heaterpower = Parameter("current heater power",
|
||||
datatype=FloatRange(0), default=0, unit="W",
|
||||
group='heater_settings',
|
||||
),
|
||||
target = Parameter("target temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
readonly=False,
|
||||
),
|
||||
value = Parameter("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
test='TEST',
|
||||
),
|
||||
pid = Parameter("regulation coefficients",
|
||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||
FloatRange(0, 100)),
|
||||
default=(40, 10, 2), readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
pid=Parameter("regulation coefficients",
|
||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||
FloatRange(0, 100)),
|
||||
default=(40, 10, 2), readonly=False,
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
p=Parameter("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
i=Parameter("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
d=Parameter("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
mode=Parameter("mode of regulation",
|
||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||
default='ramp',
|
||||
readonly=False,
|
||||
),
|
||||
pollinterval=Override("polling interval",
|
||||
datatype=FloatRange(0), default=5,
|
||||
),
|
||||
tolerance=Parameter("temperature range for stability checking",
|
||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||
i = Parameter("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
d = Parameter("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
mode = Parameter("mode of regulation",
|
||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||
default='ramp',
|
||||
readonly=False,
|
||||
),
|
||||
pollinterval = Parameter("polling interval",
|
||||
datatype=FloatRange(0), default=5,
|
||||
),
|
||||
tolerance = Parameter("temperature range for stability checking",
|
||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||
readonly=False,
|
||||
group='stability',
|
||||
),
|
||||
window = Parameter("time window for stability checking",
|
||||
datatype=FloatRange(1, 900), default=30, unit='s',
|
||||
readonly=False,
|
||||
group='stability',
|
||||
),
|
||||
timeout = Parameter("max waiting time for stabilisation check",
|
||||
datatype=FloatRange(1, 36000), default=900, unit='s',
|
||||
readonly=False,
|
||||
group='stability',
|
||||
),
|
||||
window=Parameter("time window for stability checking",
|
||||
datatype=FloatRange(1, 900), default=30, unit='s',
|
||||
readonly=False,
|
||||
group='stability',
|
||||
),
|
||||
timeout=Parameter("max waiting time for stabilisation check",
|
||||
datatype=FloatRange(1, 36000), default=900, unit='s',
|
||||
readonly=False,
|
||||
group='stability',
|
||||
),
|
||||
)
|
||||
commands = dict(
|
||||
stop=Override(
|
||||
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"),
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
self._stopflag = False
|
||||
@ -180,8 +173,11 @@ class Cryostat(CryoBase):
|
||||
def read_pid(self):
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
def do_stop(self):
|
||||
# stop the ramp by setting current setpoint as target
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""Stop ramping the setpoint
|
||||
|
||||
by setting the current setpoint as new target"""
|
||||
# XXX: discussion: take setpoint or current value ???
|
||||
self.write_target(self.setpoint)
|
||||
|
||||
|
@ -28,42 +28,39 @@ import time
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Switch(Drivable):
|
||||
"""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,
|
||||
)
|
||||
target = Parameter('wanted state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
),
|
||||
'target': Override('wanted state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'switch_on_time': Parameter('seconds to wait after activating the switch',
|
||||
readonly=False,
|
||||
)
|
||||
switch_on_time = Parameter('seconds to wait after activating the switch',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
)
|
||||
switch_off_time = Parameter('cool-down time in seconds',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
),
|
||||
'switch_off_time': Parameter('cool-down time in seconds',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
properties = {
|
||||
'description' : Property('The description of the Module', StringType(),
|
||||
default='no description', mandatory=False, extname='description'),
|
||||
}
|
||||
description = Property('The description of the Module', StringType(),
|
||||
default='no description', mandatory=False, extname='description')
|
||||
|
||||
def read_value(self):
|
||||
# could ask HW
|
||||
@ -109,30 +106,29 @@ class Switch(Drivable):
|
||||
class MagneticField(Drivable):
|
||||
"""a liquid magnet
|
||||
"""
|
||||
parameters = {
|
||||
'value': Override('current field in T',
|
||||
|
||||
value = Parameter('current field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
)
|
||||
target = Parameter('target field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
),
|
||||
'target': Override('target field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'ramp': Parameter('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
'mode': Parameter('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
),
|
||||
'heatswitch': Parameter('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
}
|
||||
readonly=False,
|
||||
)
|
||||
ramp = Parameter('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
mode = Parameter('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
)
|
||||
heatswitch = Parameter('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
|
||||
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):
|
||||
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()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
self.write_target(self.read_value())
|
||||
|
||||
|
||||
class CoilTemp(Readable):
|
||||
"""a coil temperature
|
||||
"""
|
||||
parameters = {
|
||||
'value': Override('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
),
|
||||
'sensor': Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
}
|
||||
|
||||
value = Parameter('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(2.3 + random.random(), 3)
|
||||
@ -225,18 +220,17 @@ class CoilTemp(Readable):
|
||||
class SampleTemp(Drivable):
|
||||
"""a sample temperature
|
||||
"""
|
||||
parameters = {
|
||||
'value': Override('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
),
|
||||
'sensor': Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
'ramp': Parameter('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
}
|
||||
|
||||
value = Parameter('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
ramp = Parameter('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
@ -272,20 +266,19 @@ class Label(Readable):
|
||||
of several subdevices. used for demoing connections between
|
||||
modules.
|
||||
"""
|
||||
parameters = {
|
||||
'system': Parameter("Name of the magnet system",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'subdev_mf': Parameter("name of subdevice for magnet status",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'subdev_ts': Parameter("name of subdevice for sample temp",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'value': Override("final value of label string", default='',
|
||||
datatype=StringType(),
|
||||
),
|
||||
}
|
||||
|
||||
system = Parameter("Name of the magnet system",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_mf = Parameter("name of subdevice for magnet status",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_ts = Parameter("name of subdevice for sample temp",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
value = Parameter("final value of label string", default='',
|
||||
datatype=StringType(),
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
strings = [self.system]
|
||||
@ -317,29 +310,25 @@ class Label(Readable):
|
||||
class DatatypesTest(Readable):
|
||||
"""for demoing all datatypes
|
||||
"""
|
||||
parameters = {
|
||||
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||
readonly=False, default=1),
|
||||
'tupleof': Parameter('tuple of int, float and str',
|
||||
datatype=TupleOf(IntRange(), FloatRange(),
|
||||
StringType()),
|
||||
readonly=False, default=(1, 2.3, 'a')),
|
||||
'arrayof': Parameter('array: 2..3 times bool',
|
||||
datatype=ArrayOf(BoolType(), 2, 3),
|
||||
readonly=False, default=[1, 0, 1]),
|
||||
'intrange': Parameter('intrange', datatype=IntRange(2, 9),
|
||||
readonly=False, default=4),
|
||||
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||
readonly=False, default=0, ),
|
||||
'struct': Parameter('struct(a=str, b=int, c=bool)',
|
||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||
c=BoolType()),
|
||||
),
|
||||
}
|
||||
|
||||
enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||
readonly=False, default=1)
|
||||
tupleof = Parameter('tuple of int, float and str',
|
||||
datatype=TupleOf(IntRange(), FloatRange(),
|
||||
StringType()),
|
||||
readonly=False, default=(1, 2.3, 'a'))
|
||||
arrayof = Parameter('array: 2..3 times bool',
|
||||
datatype=ArrayOf(BoolType(), 2, 3),
|
||||
readonly=False, default=[1, 0, 1])
|
||||
intrange = Parameter('intrange', datatype=IntRange(2, 9),
|
||||
readonly=False, default=4)
|
||||
floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||
readonly=False, default=0)
|
||||
struct = Parameter('struct(a=str, b=int, c=bool)',
|
||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||
c=BoolType()))
|
||||
|
||||
|
||||
class ArrayTest(Readable):
|
||||
parameters = {
|
||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default = 100000 * [0]),
|
||||
}
|
||||
x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default=100000 * [0])
|
||||
|
@ -24,7 +24,7 @@
|
||||
import random
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -45,11 +45,10 @@ class Heater(Drivable):
|
||||
class name indicates it to be some heating element,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
parameters = {
|
||||
'maxheaterpower': Parameter('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
),
|
||||
}
|
||||
|
||||
maxheaterpower = Parameter('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
@ -64,22 +63,21 @@ class Temp(Drivable):
|
||||
class name indicates it to be some temperature controller,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
parameters = {
|
||||
'sensor': Parameter(
|
||||
"Sensor number or calibration id",
|
||||
datatype=StringType(
|
||||
8,
|
||||
16),
|
||||
readonly=True,
|
||||
),
|
||||
'target': Override(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
),
|
||||
}
|
||||
|
||||
sensor = Parameter(
|
||||
"Sensor number or calibration id",
|
||||
datatype=StringType(
|
||||
8,
|
||||
16),
|
||||
readonly=True,
|
||||
)
|
||||
target = Parameter(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
@ -90,8 +88,8 @@ class Temp(Drivable):
|
||||
|
||||
class Lower(Communicator):
|
||||
"""Communicator returning a lowercase version of the request"""
|
||||
command = {
|
||||
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'),
|
||||
}
|
||||
def do_communicate(self, request):
|
||||
return str(request).lower()
|
||||
|
||||
@Command(argument=StringType(), result=StringType(), export='communicate')
|
||||
def communicate(self, command):
|
||||
"""lowercase a string"""
|
||||
return str(command).lower()
|
||||
|
@ -58,20 +58,20 @@ except ImportError:
|
||||
class EpicsReadable(Readable):
|
||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||
# Commmon parameter for all EPICS devices
|
||||
parameters = {
|
||||
'value': Parameter('EPICS generic value',
|
||||
datatype=FloatRange(),
|
||||
default=300.0,),
|
||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
||||
datatype=EnumType(v3=3, v4=4),),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'value_pv': Parameter('EPICS pv_name of value',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'status_pv': Parameter('EPICS pv_name of status',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
value = Parameter('EPICS generic value',
|
||||
datatype=FloatRange(),
|
||||
default=300.0,)
|
||||
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||
datatype=EnumType(v3=3, v4=4),)
|
||||
value_pv = Parameter('EPICS pv_name of value',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
status_pv = Parameter('EPICS pv_name of status',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
|
||||
class EpicsDrivable(Drivable):
|
||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||
# Commmon parameter for all EPICS devices
|
||||
parameters = {
|
||||
'target': Parameter('EPICS generic target', datatype=FloatRange(),
|
||||
default=300.0, readonly=False),
|
||||
'value': Parameter('EPICS generic value', datatype=FloatRange(),
|
||||
default=300.0,),
|
||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
||||
datatype=StringType(),),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
target = Parameter('EPICS generic target', datatype=FloatRange(),
|
||||
default=300.0, readonly=False)
|
||||
value = Parameter('EPICS generic value', datatype=FloatRange(),
|
||||
default=300.0,)
|
||||
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||
datatype=StringType(),)
|
||||
target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
|
||||
|
||||
class EpicsTempCtrl(EpicsDrivable):
|
||||
|
||||
parameters = {
|
||||
# TODO: restrict possible values with oneof datatype
|
||||
'heaterrange': Parameter('Heater range', datatype=StringType(),
|
||||
default='Off', readonly=False,),
|
||||
'tolerance': Parameter('allowed deviation between value and target',
|
||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||
readonly=False,),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'heaterrange_pv': Parameter('EPICS pv_name of heater range',
|
||||
datatype=StringType(), default="unset", export=False,),
|
||||
}
|
||||
|
||||
# parameters
|
||||
heaterrange = Parameter('Heater range', datatype=StringType(),
|
||||
default='Off', readonly=False,)
|
||||
tolerance = Parameter('allowed deviation between value and target',
|
||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||
readonly=False,)
|
||||
heaterrange_pv = Parameter('EPICS pv_name of heater range',
|
||||
datatype=StringType(), default="unset", export=False,)
|
||||
|
||||
|
||||
def read_target(self):
|
||||
return self._read_pv(self.target_pv)
|
||||
|
@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
parameters = {
|
||||
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False),
|
||||
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False),
|
||||
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False),
|
||||
'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='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10),
|
||||
'abslimits': Parameter('Absolute limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(-0.5, 0.5), poll=True,
|
||||
),
|
||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
||||
),
|
||||
'ramp': Parameter('Target rate of field change per minute', readonly=False,
|
||||
datatype=FloatRange(unit='$/min'), default=1.0),
|
||||
'calibration': Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0)),
|
||||
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||
short=ArrayOf(
|
||||
FloatRange(), 5, 5),
|
||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
|
||||
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='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(-0.5, 0.5), poll=True,
|
||||
)
|
||||
precision = Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
||||
)
|
||||
ramp = Parameter('Target rate of field change per minute', readonly=False,
|
||||
datatype=FloatRange(unit='$/min'), default=1.0)
|
||||
calibration = Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||
short=ArrayOf(
|
||||
FloatRange(), 5, 5),
|
||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
|
||||
|
||||
|
||||
def _current2field(self, current, *coefficients):
|
||||
"""Return field in T for given current in A.
|
||||
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
return self._currentsource.read_status()[0] == 'BUSY'
|
||||
if self._currentsource.status[0] != 'BUSY':
|
||||
if self._enable.status[0] == 'ERROR':
|
||||
self._enable.do_reset()
|
||||
self._enable.reset()
|
||||
self._enable.read_status()
|
||||
self._enable.write_target('On')
|
||||
self._enable._hw_wait()
|
||||
|
@ -41,7 +41,7 @@ from secop.errors import CommunicationFailedError, \
|
||||
ConfigError, HardwareError, ProgrammingError
|
||||
from secop.lib import lazy_property
|
||||
from secop.modules import Command, Drivable, \
|
||||
Module, Override, Parameter, Readable, BasicPoller
|
||||
Module, Parameter, Readable, BasicPoller
|
||||
|
||||
#####
|
||||
|
||||
@ -160,24 +160,18 @@ class PyTangoDevice(Module):
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
parameters = {
|
||||
'comtries': Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
group='communication'),
|
||||
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0),
|
||||
unit='s', default=0.1, readonly=False,
|
||||
group='communication'),
|
||||
|
||||
'tangodevice': Parameter('Tango device name',
|
||||
datatype=StringType(), readonly=True,
|
||||
# export=True, # for testing only
|
||||
export=False,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reset': Command('Tango reset command', argument=None, result=None),
|
||||
}
|
||||
# parameters
|
||||
comtries = Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
group='communication')
|
||||
comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
|
||||
unit='s', default=0.1, readonly=False,
|
||||
group='communication')
|
||||
tangodevice = Parameter('Tango device name',
|
||||
datatype=StringType(), readonly=True,
|
||||
# export=True, # for testing only
|
||||
export=False,
|
||||
)
|
||||
|
||||
tango_status_mapping = {
|
||||
PyTango.DevState.ON: Drivable.Status.IDLE,
|
||||
@ -372,7 +366,9 @@ class PyTangoDevice(Module):
|
||||
|
||||
return (myState, tangoStatus)
|
||||
|
||||
def do_reset(self):
|
||||
@Command(argument=None, result=None)
|
||||
def reset(self):
|
||||
"""Tango reset command"""
|
||||
self._dev.Reset()
|
||||
|
||||
|
||||
@ -405,13 +401,9 @@ class Sensor(AnalogInput):
|
||||
# note: we don't transport the formula to secop....
|
||||
# we support the adjust method
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
|
||||
def do_setposition(self, value):
|
||||
@Command(argument=FloatRange(), result=None)
|
||||
def setposition(self, value):
|
||||
"""Set the position to the given value."""
|
||||
self._dev.Adjust(value)
|
||||
|
||||
|
||||
@ -427,29 +419,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
controllers, ...
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'userlimits': Parameter('User defined limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')),
|
||||
readonly=False, poll=10,
|
||||
),
|
||||
'abslimits': Parameter('Absolute limits of device value',
|
||||
# parameters
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
),
|
||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(1e-38, unit='$'),
|
||||
readonly=False, group='stability',
|
||||
),
|
||||
'window': Parameter('Time window for checking stabilization if > 0',
|
||||
default=60.0, readonly=False,
|
||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||
),
|
||||
'timeout': Parameter('Timeout for waiting for a stable value (if > 0)',
|
||||
default=60.0, readonly=False,
|
||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||
),
|
||||
}
|
||||
default=(float('-Inf'), float('+Inf')),
|
||||
readonly=False, poll=10,
|
||||
)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
)
|
||||
precision = Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(1e-38, unit='$'),
|
||||
readonly=False, group='stability',
|
||||
)
|
||||
window = Parameter('Time window for checking stabilization if > 0',
|
||||
default=60.0, readonly=False,
|
||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||
)
|
||||
timeout = Parameter('Timeout for waiting for a stable value (if > 0)',
|
||||
default=60.0, readonly=False,
|
||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||
)
|
||||
|
||||
_history = ()
|
||||
_timeout = None
|
||||
_moving = False
|
||||
@ -566,7 +558,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if self.status[0] == self.Status.BUSY:
|
||||
# changing target value during movement is not allowed by the
|
||||
# Tango base class state machine. If we are moving, stop first.
|
||||
self.do_stop()
|
||||
self.stop()
|
||||
self._hw_wait()
|
||||
self._dev.value = value
|
||||
# set meaningful timeout
|
||||
@ -587,7 +579,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||
sleep(0.3)
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
self._dev.Stop()
|
||||
|
||||
|
||||
@ -601,21 +593,14 @@ class Actuator(AnalogOutput):
|
||||
"""
|
||||
# for secop: support the speed and ramp parameters
|
||||
|
||||
parameters = {
|
||||
'speed': Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
),
|
||||
'ramp': Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
# parameters
|
||||
speed = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
)
|
||||
ramp = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
)
|
||||
|
||||
def read_speed(self):
|
||||
return self._dev.speed
|
||||
@ -630,7 +615,9 @@ class Actuator(AnalogOutput):
|
||||
self.write_speed(value / 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)
|
||||
|
||||
|
||||
@ -641,21 +628,16 @@ class Motor(Actuator):
|
||||
It has the ability to move a real object from one place to another place.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'refpos': Parameter('Reference position',
|
||||
datatype=FloatRange(unit='$'),
|
||||
),
|
||||
'accel': Parameter('Acceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
),
|
||||
'decel': Parameter('Deceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reference': Command('Do a reference run', argument=None, result=None),
|
||||
}
|
||||
# parameters
|
||||
refpos = Parameter('Reference position',
|
||||
datatype=FloatRange(unit='$'),
|
||||
)
|
||||
accel = Parameter('Acceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
)
|
||||
decel = Parameter('Deceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
)
|
||||
|
||||
def read_refpos(self):
|
||||
return float(self._getProperty('refpos'))
|
||||
@ -672,7 +654,9 @@ class Motor(Actuator):
|
||||
def write_decel(self, value):
|
||||
self._dev.decel = value
|
||||
|
||||
def do_reference(self):
|
||||
@Command()
|
||||
def reference(self):
|
||||
"""Do a reference run"""
|
||||
self._dev.Reference()
|
||||
return self.read_value()
|
||||
|
||||
@ -681,32 +665,29 @@ class TemperatureController(Actuator):
|
||||
"""A temperature control loop device.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'p': Parameter('Proportional control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
),
|
||||
'i': Parameter('Integral control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
),
|
||||
'd': Parameter('Derivative control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
),
|
||||
'pid': Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
),
|
||||
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
),
|
||||
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
),
|
||||
}
|
||||
# parameters
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter('Proportional control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
)
|
||||
i = Parameter('Integral control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
)
|
||||
d = Parameter('Derivative control Parameter', datatype=FloatRange(),
|
||||
readonly=False, group='pid',
|
||||
)
|
||||
pid = Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
)
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
)
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
)
|
||||
|
||||
overrides = {
|
||||
# We want this to be freely user-settable, and not produce a warning
|
||||
# on startup, so select a usually sensible default.
|
||||
'precision': Override(default=0.1),
|
||||
'ramp': Override(description='Temperature ramp'),
|
||||
}
|
||||
# overrides
|
||||
precision = Parameter(default=0.1)
|
||||
ramp = Parameter(description='Temperature ramp')
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
@ -755,15 +736,14 @@ class PowerSupply(Actuator):
|
||||
"""A power supply (voltage and current) device.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'voltage': Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5),
|
||||
'current': Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5),
|
||||
}
|
||||
overrides = {
|
||||
'ramp': Override(description='Current/voltage ramp'),
|
||||
}
|
||||
# parameters
|
||||
voltage = Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5)
|
||||
current = Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5)
|
||||
|
||||
# overrides
|
||||
ramp = Parameter(description='Current/voltage ramp')
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
@ -782,9 +762,8 @@ class DigitalInput(PyTangoDevice, Readable):
|
||||
"""A device reading a bitfield.
|
||||
"""
|
||||
|
||||
overrides = {
|
||||
'value': Override(datatype=IntRange()),
|
||||
}
|
||||
# overrides
|
||||
value = Parameter(datatype=IntRange())
|
||||
|
||||
def read_value(self):
|
||||
return self._dev.value
|
||||
@ -794,10 +773,9 @@ class NamedDigitalInput(DigitalInput):
|
||||
"""A DigitalInput with numeric values mapped to names.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False), # XXX:!!!
|
||||
}
|
||||
# parameters
|
||||
mapping = Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False) # XXX:!!!
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalInput, self).initModule()
|
||||
@ -821,12 +799,11 @@ class PartialDigitalInput(NamedDigitalInput):
|
||||
bit width accessed.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'startbit': Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0),
|
||||
'bitwidth': Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1),
|
||||
}
|
||||
# parameters
|
||||
startbit = Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0)
|
||||
bitwidth = Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalInput, self).initModule()
|
||||
@ -844,10 +821,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
||||
bitfield.
|
||||
"""
|
||||
|
||||
overrides = {
|
||||
'value': Override(datatype=IntRange()),
|
||||
'target': Override(datatype=IntRange()),
|
||||
}
|
||||
# overrides
|
||||
value = Parameter(datatype=IntRange())
|
||||
target = Parameter(datatype=IntRange())
|
||||
|
||||
def read_value(self):
|
||||
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.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False),
|
||||
}
|
||||
# parameters
|
||||
mapping = Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False)
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalOutput, self).initModule()
|
||||
@ -894,12 +869,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
bit width accessed.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'startbit': Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0),
|
||||
'bitwidth': Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1),
|
||||
}
|
||||
# parameters
|
||||
startbit = Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0)
|
||||
bitwidth = Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalOutput, self).initModule()
|
||||
@ -925,17 +899,16 @@ class StringIO(PyTangoDevice, Module):
|
||||
receives strings.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'bustimeout': Parameter('Communication timeout',
|
||||
datatype=FloatRange(unit='s'), readonly=False,
|
||||
group='communication'),
|
||||
'endofline': Parameter('End of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication'),
|
||||
'startofline': Parameter('Start of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication'),
|
||||
}
|
||||
# parameters
|
||||
bustimeout = Parameter('Communication timeout',
|
||||
datatype=FloatRange(unit='s'), readonly=False,
|
||||
group='communication')
|
||||
endofline = Parameter('End of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication')
|
||||
startofline = Parameter('Start of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication')
|
||||
|
||||
def read_bustimeout(self):
|
||||
return self._dev.communicationTimeout
|
||||
@ -955,53 +928,48 @@ class StringIO(PyTangoDevice, Module):
|
||||
def write_startofline(self, value):
|
||||
self._dev.startOfLine = value
|
||||
|
||||
commands = {
|
||||
'communicate': Command('Send a string and return the reply',
|
||||
argument=StringType(),
|
||||
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()):
|
||||
@Command(argument=StringType(), result=StringType())
|
||||
def communicate(self, value=StringType()):
|
||||
"""Send a string and return the reply"""
|
||||
return self._dev.Communicate(value)
|
||||
|
||||
def do_flush(self):
|
||||
@Command(argument=None, result=None)
|
||||
def flush(self):
|
||||
"""Flush output buffer"""
|
||||
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)
|
||||
|
||||
def do_write(self, value):
|
||||
@Command(argument=StringType(), result=None)
|
||||
def write(self, value):
|
||||
"""write some chars to output"""
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def do_availableChars(self):
|
||||
@Command(argument=None, result=IntRange(0))
|
||||
def availableChars(self):
|
||||
"""return number of chars in input buffer"""
|
||||
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
|
||||
|
@ -20,7 +20,7 @@
|
||||
# *****************************************************************************
|
||||
"""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):
|
||||
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
|
||||
|
||||
|
||||
class Capacitance(HasIodev, Readable):
|
||||
parameters = {
|
||||
'value': Override('capacitance', FloatRange(unit='pF'), poll=True),
|
||||
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0),
|
||||
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0),
|
||||
'loss': Parameter('loss', FloatRange(unit='deg'), default=0),
|
||||
}
|
||||
|
||||
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
||||
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||
|
||||
iodevClass = Ah2700IO
|
||||
|
||||
def parse_reply(self, reply):
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -42,13 +42,13 @@ SOURCECMDS = {
|
||||
|
||||
|
||||
class SourceMeter(HasIodev, Module):
|
||||
parameters = {
|
||||
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True),
|
||||
'power': Parameter('readback power', FloatRange(unit='W'), poll=True),
|
||||
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||
readonly=False, default=0),
|
||||
'active': Parameter('output enable', BoolType(), readonly=False, poll=True),
|
||||
}
|
||||
|
||||
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
||||
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
||||
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||
readonly=False, default=0)
|
||||
active = Parameter('output enable', BoolType(), readonly=False, poll=True)
|
||||
|
||||
iodevClass = K2601bIO
|
||||
|
||||
def read_resistivity(self):
|
||||
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
|
||||
|
||||
|
||||
class Current(HasIodev, Writable):
|
||||
properties = {
|
||||
'sourcemeter': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('measured current', FloatRange(unit='A'), poll=True),
|
||||
'target': Override('set current', FloatRange(unit='A'), 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),
|
||||
}
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
||||
target = Parameter('set current', FloatRange(unit='A'), 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):
|
||||
return self.sendRecv('print(smua.measure.i())')
|
||||
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
|
||||
|
||||
|
||||
class Voltage(HasIodev, Writable):
|
||||
properties = {
|
||||
'sourcemeter': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('measured voltage', FloatRange(unit='V'), poll=True),
|
||||
'target': Override('set voltage', FloatRange(unit='V'), poll=True),
|
||||
'active': Parameter('voltage is controlled', BoolType(), poll=True),
|
||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
|
||||
}
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
||||
target = Parameter('set voltage', FloatRange(unit='V'), 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):
|
||||
return self.sendRecv('print(smua.measure.v())')
|
||||
@ -159,7 +153,7 @@ class Voltage(HasIodev, Writable):
|
||||
def write_active(self, value):
|
||||
if self._sourcemeter.mode != 2:
|
||||
if value:
|
||||
self._sourcemeter.write_mode(2) # switch to voltage
|
||||
self._sourcemeter.write_mode(2) # switch to voltage
|
||||
else:
|
||||
return 0
|
||||
return self._sourcemeter.write_active(value)
|
||||
|
@ -22,8 +22,7 @@
|
||||
|
||||
import time
|
||||
|
||||
from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached
|
||||
from secop.metaclass import Done
|
||||
from secop.modules import Readable, Drivable, Parameter, Property, Attached, Done
|
||||
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
|
||||
from secop.stringio import HasIodev
|
||||
from secop.poller import Poller, REGULAR
|
||||
@ -59,13 +58,11 @@ class StringIO(secop.stringio.StringIO):
|
||||
|
||||
|
||||
class Main(HasIodev, Drivable):
|
||||
parameters = {
|
||||
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)),
|
||||
'target': Override('channel to select', datatype=IntRange(0, 17)),
|
||||
'autoscan':
|
||||
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False),
|
||||
'pollinterval': Override('sleeptime between polls', default=1),
|
||||
}
|
||||
|
||||
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
||||
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||
pollinterval = Parameter('sleeptime between polls', default=1)
|
||||
|
||||
pollerClass = Poller
|
||||
iodevClass = StringIO
|
||||
@ -142,40 +139,23 @@ class ResChannel(HasIodev, Readable):
|
||||
_main = None # main module
|
||||
_last_range_change = 0 # time of last range change
|
||||
|
||||
properties = {
|
||||
'channel':
|
||||
Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False),
|
||||
'main':
|
||||
Attached()
|
||||
}
|
||||
channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
|
||||
main = Attached()
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='Ohm')),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
'range':
|
||||
Parameter('reading range', readonly=False,
|
||||
datatype=EnumType(**RES_RANGE), handler=rdgrng),
|
||||
'minrange':
|
||||
Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||
datatype=EnumType(**RES_RANGE)),
|
||||
'autorange':
|
||||
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||
readonly=False, handler=rdgrng, default=2),
|
||||
'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),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
range = Parameter('reading range', readonly=False,
|
||||
datatype=EnumType(**RES_RANGE), handler=rdgrng)
|
||||
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||
datatype=EnumType(**RES_RANGE))
|
||||
autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||
readonly=False, handler=rdgrng, default=2)
|
||||
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):
|
||||
self._main = self.DISPATCHER.get_module(self.main)
|
||||
|
@ -41,7 +41,7 @@ class Ls370Sim(Communicator):
|
||||
self._data[fmt % chan] = v
|
||||
# mkthread(self.run)
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
# simulation part, time independent
|
||||
for channel in range(1,17):
|
||||
_, _, _, _, 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 threading
|
||||
|
||||
from secop.modules import Module, Readable, Drivable, Parameter, Override,\
|
||||
Communicator, Property, Attached
|
||||
from secop.modules import Readable, Drivable, Parameter,\
|
||||
Communicator, Property, Attached, HasAccessibles, Done
|
||||
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
|
||||
BoolType, StatusType
|
||||
from secop.lib.enum import Enum
|
||||
@ -44,7 +44,6 @@ from secop.errors import HardwareError
|
||||
from secop.poller import Poller
|
||||
import secop.iohandler
|
||||
from secop.stringio import HasIodev
|
||||
from secop.metaclass import Done
|
||||
|
||||
try:
|
||||
import secop_psi.ppmswindows as ppmshw
|
||||
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
|
||||
class Main(Communicator):
|
||||
"""ppms communicator module"""
|
||||
|
||||
parameters = {
|
||||
'pollinterval': Parameter('poll interval', readonly=False,
|
||||
datatype=FloatRange(), default=2),
|
||||
'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()),
|
||||
}
|
||||
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
||||
default="", readonly=True)
|
||||
|
||||
_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',
|
||||
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
||||
assert len(_channel_names) == 30
|
||||
@ -102,7 +96,8 @@ class Main(Communicator):
|
||||
def register(self, other):
|
||||
self.modules[other.channel] = other
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
"""GPIB command"""
|
||||
with self.lock:
|
||||
reply = self._ppms_device.send(command)
|
||||
self.log.debug("%s|%s", command, reply)
|
||||
@ -114,7 +109,7 @@ class Main(Communicator):
|
||||
if channel.enabled:
|
||||
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
||||
# send, read and convert to floats and ints
|
||||
data = self.do_communicate('GETDAT? %d' % mask)
|
||||
data = self.communicate('GETDAT? %d' % mask)
|
||||
reply = data.split(',')
|
||||
mask = int(reply.pop(0))
|
||||
reply.pop(0) # pop timestamp
|
||||
@ -133,11 +128,9 @@ class Main(Communicator):
|
||||
return data # return data as string
|
||||
|
||||
|
||||
class PpmsMixin(HasIodev, Module):
|
||||
class PpmsMixin(HasIodev, HasAccessibles):
|
||||
"""common methods for ppms modules"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
}
|
||||
iodev = Attached()
|
||||
|
||||
pollerClass = Poller
|
||||
enabled = True # default, if no parameter enable is defined
|
||||
@ -177,28 +170,21 @@ class PpmsMixin(HasIodev, Module):
|
||||
|
||||
class Channel(PpmsMixin, Readable):
|
||||
"""channel base class"""
|
||||
parameters = {
|
||||
'value':
|
||||
Override('main value of channels', poll=True),
|
||||
'enabled':
|
||||
Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=False),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
}
|
||||
properties = {
|
||||
'channel':
|
||||
Property('channel name',
|
||||
datatype=StringType(), export=False, default=''),
|
||||
'no':
|
||||
Property('channel number',
|
||||
datatype=IntRange(1, 4), export=False),
|
||||
}
|
||||
|
||||
value = Parameter('main value of channels', poll=True)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=False)
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
channel = Property('channel name',
|
||||
datatype=StringType(), export=False, default='')
|
||||
no = Property('channel number',
|
||||
datatype=IntRange(1, 4), export=False)
|
||||
|
||||
def earlyInit(self):
|
||||
Readable.earlyInit(self)
|
||||
if not self.channel:
|
||||
self.properties['channel'] = self.name
|
||||
self.channel = self.name
|
||||
|
||||
def get_settings(self, pname):
|
||||
return ''
|
||||
@ -207,19 +193,12 @@ class Channel(PpmsMixin, Readable):
|
||||
class UserChannel(Channel):
|
||||
"""user channel"""
|
||||
|
||||
parameters = {
|
||||
'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=''),
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
}
|
||||
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):
|
||||
other = self._iodev.modules.get(self.linkenable, None)
|
||||
@ -233,16 +212,11 @@ class DriverChannel(Channel):
|
||||
|
||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||
|
||||
parameters = {
|
||||
'current':
|
||||
Parameter('driver current', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 5000., unit='uA')),
|
||||
'powerlimit':
|
||||
Parameter('power limit', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 1000., unit='uW')),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
}
|
||||
current = Parameter('driver current', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 1000., unit='uW'))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
def analyze_drvout(self, no, current, powerlimit):
|
||||
if self.no != no:
|
||||
@ -260,27 +234,19 @@ class BridgeChannel(Channel):
|
||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||
# pylint: disable=invalid-name
|
||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||
parameters = {
|
||||
'enabled':
|
||||
Override(handler=bridge),
|
||||
'excitation':
|
||||
Parameter('excitation current', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.01, 5000., unit='uA')),
|
||||
'powerlimit':
|
||||
Parameter('power limit', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.001, 1000., unit='uW')),
|
||||
'dcflag':
|
||||
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||
datatype=BoolType()),
|
||||
'readingmode':
|
||||
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),
|
||||
}
|
||||
|
||||
enabled = Parameter(handler=bridge)
|
||||
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||
datatype=BoolType())
|
||||
readingmode = 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 = Parameter(visibility=3)
|
||||
|
||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
||||
if self.no != no:
|
||||
@ -306,12 +272,9 @@ class Level(PpmsMixin, Readable):
|
||||
|
||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange(unit='%'), handler=level),
|
||||
'status': Override(handler=level),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
||||
status = Parameter(handler=level)
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
channel = 'level'
|
||||
|
||||
@ -360,16 +323,13 @@ class Chamber(PpmsMixin, Drivable):
|
||||
venting_continuously=9,
|
||||
general_failure=15,
|
||||
)
|
||||
parameters = {
|
||||
'value':
|
||||
Override(description='chamber state', handler=chamber,
|
||||
datatype=EnumType(StatusCode)),
|
||||
'target':
|
||||
Override(description='chamber command', handler=chamber,
|
||||
datatype=EnumType(Operation)),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
}
|
||||
|
||||
value = Parameter(description='chamber state', handler=chamber,
|
||||
datatype=EnumType(StatusCode))
|
||||
target = Parameter(description='chamber command', handler=chamber,
|
||||
datatype=EnumType(Operation))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
||||
@ -409,37 +369,29 @@ class Temp(PpmsMixin, Drivable):
|
||||
"""temperature"""
|
||||
|
||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||
Status = Enum(Drivable.Status,
|
||||
RAMPING = 370,
|
||||
STABILIZING = 380,
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='K'), poll=True),
|
||||
'status':
|
||||
Override(datatype=StatusType(Status), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False),
|
||||
'setpoint':
|
||||
Parameter('intermediate set point',
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp),
|
||||
'ramp':
|
||||
Parameter('ramping speed', readonly=False, default=0,
|
||||
datatype=FloatRange(0, 20, unit='K/min')),
|
||||
'workingramp':
|
||||
Parameter('intermediate ramp value',
|
||||
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),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
||||
setpoint = Parameter('intermediate set point',
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
||||
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||
datatype=FloatRange(0, 20, unit='K/min'))
|
||||
workingramp = Parameter('intermediate ramp value',
|
||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
||||
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
||||
datatype=EnumType(ApproachMode))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
TempStatus = Enum(
|
||||
'TempStatus',
|
||||
@ -464,17 +416,14 @@ class Temp(PpmsMixin, Drivable):
|
||||
14: (Status.ERROR, 'can not complete'),
|
||||
15: (Status.ERROR, 'general failure'),
|
||||
}
|
||||
properties = {
|
||||
'general_stop': Property('respect general stop', datatype=BoolType(),
|
||||
export=True, default=True)
|
||||
}
|
||||
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||
default=True, value=False)
|
||||
|
||||
channel = 'temp'
|
||||
_stopped = False
|
||||
_expected_target_time = 0
|
||||
_last_change = 0 # 0 means no target change is pending
|
||||
_last_target = None # last reached target
|
||||
general_stop = False
|
||||
_cool_deadline = 0
|
||||
_wait_at10 = False
|
||||
_ramp_at_limit = False
|
||||
@ -588,7 +537,7 @@ class Temp(PpmsMixin, Drivable):
|
||||
def calc_expected(self, target, 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():
|
||||
return
|
||||
if self.status[0] != self.Status.STABILIZING:
|
||||
@ -605,35 +554,27 @@ class Field(PpmsMixin, Drivable):
|
||||
"""magnetic field"""
|
||||
|
||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||
Status = Enum(Drivable.Status,
|
||||
PREPARED = 150,
|
||||
PREPARING = 340,
|
||||
RAMPING = 370,
|
||||
FINALIZING = 390,
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
PREPARED=150,
|
||||
PREPARING=340,
|
||||
RAMPING=370,
|
||||
FINALIZING=390,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='T'), poll=True),
|
||||
'status':
|
||||
Override(datatype=StatusType(Status), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field),
|
||||
'ramp':
|
||||
Parameter('ramping speed', readonly=False, handler=field,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min')),
|
||||
'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),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
||||
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
||||
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 = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'persistent mode'),
|
||||
@ -669,7 +610,7 @@ class Field(PpmsMixin, Drivable):
|
||||
else:
|
||||
status = (self.Status.WARN, 'timeout when ramping leads')
|
||||
elif now > self._last_change + 5:
|
||||
self._last_change = 0 # give up waiting for driving
|
||||
self._last_change = 0 # give up waiting for driving
|
||||
elif self.isDriving(status) and status != self._status_before_change:
|
||||
self._last_change = 0
|
||||
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
||||
@ -735,7 +676,7 @@ class Field(PpmsMixin, Drivable):
|
||||
return Done
|
||||
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():
|
||||
return
|
||||
newtarget = clamp(self._last_target, self.value, self.target)
|
||||
@ -751,20 +692,15 @@ class Position(PpmsMixin, Drivable):
|
||||
|
||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
||||
Status = Drivable.Status
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='deg'), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move),
|
||||
'enabled':
|
||||
Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=True),
|
||||
'speed':
|
||||
Parameter('motor speed', readonly=False, handler=move,
|
||||
datatype=FloatRange(0.8, 12, unit='deg/sec')),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=True)
|
||||
speed = Parameter('motor speed', readonly=False, handler=move,
|
||||
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'at target'),
|
||||
5: (Status.BUSY, 'moving'),
|
||||
@ -843,7 +779,7 @@ class Position(PpmsMixin, Drivable):
|
||||
self.speed = value
|
||||
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():
|
||||
return
|
||||
newtarget = clamp(self._last_target, self.value, self.target)
|
||||
|
@ -26,7 +26,7 @@ import math
|
||||
import numpy as np
|
||||
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):
|
||||
@ -102,6 +102,7 @@ class CalCurve:
|
||||
sensopt = calibspec.split(',')
|
||||
calibname = sensopt.pop(0)
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
kind = None
|
||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
||||
# first try without adding kind
|
||||
filename = join(path.strip(), calibname)
|
||||
@ -150,16 +151,14 @@ class CalCurve:
|
||||
|
||||
|
||||
class Sensor(Readable):
|
||||
properties = {
|
||||
'rawsensor': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'calib': Parameter('calibration name', datatype=StringType(), readonly=False),
|
||||
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True),
|
||||
'value': Override(unit='K'),
|
||||
'pollinterval': Override(export=False),
|
||||
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
}
|
||||
rawsensor = Attached()
|
||||
|
||||
calib = Parameter('calibration name', datatype=StringType(), readonly=False)
|
||||
abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
|
||||
value = Parameter(unit='K')
|
||||
pollinterval = Parameter(export=False)
|
||||
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
|
||||
pollerClass = None
|
||||
description = 'a calibrated sensor value'
|
||||
_value_error = None
|
||||
|
@ -25,7 +25,7 @@
|
||||
# no fixtures needed
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, Enum, StatusType, \
|
||||
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
||||
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
||||
|
||||
@ -359,6 +359,7 @@ def test_BoolType():
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
BoolType(unit='K')
|
||||
|
||||
|
||||
def test_ArrayOf():
|
||||
# test constructor catching illegal arguments
|
||||
with pytest.raises(ValueError):
|
||||
@ -478,6 +479,14 @@ def test_Command():
|
||||
'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():
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(1)
|
||||
|
@ -107,15 +107,11 @@ def test_IOHandler():
|
||||
|
||||
|
||||
class Module1(Module):
|
||||
properties = {
|
||||
'channel': Property('the channel', IntRange(), default=3),
|
||||
'loop': Property('the loop', IntRange(), default=2),
|
||||
}
|
||||
parameters = {
|
||||
'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),
|
||||
}
|
||||
channel = Property('the channel', IntRange(), default=3)
|
||||
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)
|
||||
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||
|
||||
def sendRecv(self, 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
|
||||
# pylint: disable=unused-variable
|
||||
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."""
|
||||
|
||||
# no fixtures needed
|
||||
#import pytest
|
||||
|
||||
import threading
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, FloatRange, StringType
|
||||
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.errors import ProgrammingError
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
@ -64,30 +64,27 @@ def test_Communicator():
|
||||
assert event.is_set() # event should be set immediately
|
||||
|
||||
|
||||
def test_ModuleMeta():
|
||||
def test_ModuleMagic():
|
||||
class Newclass1(Drivable):
|
||||
parameters = {
|
||||
'pollinterval': Override(reorder=True),
|
||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
||||
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
||||
}
|
||||
commands = {
|
||||
# intermixing parameters with commands is not recommended,
|
||||
# but acceptable for influencing the order
|
||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
||||
'value': Override(datatype=StringType(), default='first'),
|
||||
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
|
||||
}
|
||||
param1 = Parameter('param1', datatype=BoolType(), default=False)
|
||||
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
|
||||
|
||||
@Command(argument=BoolType(), result=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')
|
||||
|
||||
@Command(argument=BoolType(), result=BoolType())
|
||||
def cmd2(self, arg):
|
||||
"""another stuff"""
|
||||
return not arg
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def do_cmd(self, arg):
|
||||
return not arg
|
||||
|
||||
def do_cmd2(self, arg):
|
||||
return not arg
|
||||
|
||||
def read_param1(self):
|
||||
return True
|
||||
|
||||
@ -103,19 +100,31 @@ def test_ModuleMeta():
|
||||
def read_value(self):
|
||||
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
|
||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
||||
with pytest.raises(ProgrammingError):
|
||||
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']
|
||||
|
||||
class Newclass2(Newclass1):
|
||||
parameters = {
|
||||
'cmd2': Override('another stuff'),
|
||||
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
|
||||
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
|
||||
poll=True, readonly=False, initwrite=True),
|
||||
}
|
||||
paramOrder = 'param1', 'param2', 'cmd', 'value'
|
||||
|
||||
@Command(description='another stuff')
|
||||
def cmd2(self, arg):
|
||||
return arg
|
||||
|
||||
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):
|
||||
self._a1_written = value
|
||||
@ -128,47 +137,15 @@ def test_ModuleMeta():
|
||||
def read_value(self):
|
||||
return 0
|
||||
|
||||
sortcheck2 = ['status', 'target', 'pollinterval',
|
||||
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'value', 'a1', '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()
|
||||
# first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
|
||||
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
|
||||
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
|
||||
|
||||
logger = LoggerStub()
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
params_found = set() # set of instance accessibles
|
||||
params_found = set() # set of instance accessibles
|
||||
objects = []
|
||||
|
||||
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
|
||||
@ -176,15 +153,11 @@ def test_ModuleMeta():
|
||||
o2 = newclass('o2', logger, {'.description':''}, srv)
|
||||
for obj in [o1, o2]:
|
||||
objects.append(obj)
|
||||
ctr_found = set()
|
||||
for n, o in obj.accessibles.items():
|
||||
for o in obj.accessibles.values():
|
||||
# check that instance accessibles are unique objects
|
||||
assert o not in params_found
|
||||
params_found.add(o)
|
||||
assert o.ctr not in ctr_found
|
||||
ctr_found.add(o.ctr)
|
||||
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
|
||||
assert check_order == sorted(check_order)
|
||||
assert list(obj.accessibles) == sortcheck
|
||||
|
||||
# check for inital updates working properly
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
@ -246,7 +219,7 @@ def test_ModuleMeta():
|
||||
assert acs is not None
|
||||
else: # do not check object or mixin
|
||||
acs = {}
|
||||
for n, o in acs.items():
|
||||
for o in acs.values():
|
||||
# check that class accessibles are not reused as instance accessibles
|
||||
assert o not in params_found
|
||||
|
||||
|
@ -25,68 +25,78 @@
|
||||
# no fixtures needed
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, IntRange
|
||||
from secop.params import Command, Override, Parameter, Parameters
|
||||
from secop.datatypes import BoolType, IntRange, FloatRange
|
||||
from secop.params import Command, Parameter
|
||||
from secop.modules import HasAccessibles
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
def test_Command():
|
||||
cmd = Command('do_something')
|
||||
assert cmd.description == 'do_something'
|
||||
assert cmd.ctr
|
||||
assert cmd.argument is None
|
||||
assert cmd.result is None
|
||||
assert cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||
'description': 'do_something'}
|
||||
class Mod(HasAccessibles):
|
||||
@Command()
|
||||
def cmd(self):
|
||||
"""do something"""
|
||||
@Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
|
||||
def cmd2(self):
|
||||
pass
|
||||
|
||||
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1))
|
||||
assert cmd.description
|
||||
assert isinstance(cmd.argument, IntRange)
|
||||
assert isinstance(cmd.result, IntRange)
|
||||
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9},
|
||||
'result': {'type': 'int', 'min':-1, 'max':1}},
|
||||
'description': 'do_something'}
|
||||
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
||||
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
||||
'description': 'do_something'}
|
||||
assert Mod.cmd.description == 'do something'
|
||||
assert Mod.cmd.argument is None
|
||||
assert Mod.cmd.result is None
|
||||
assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||
'description': 'do something'}
|
||||
|
||||
assert Mod.cmd2.description == 'do some other thing'
|
||||
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}},
|
||||
'description': 'do some other thing'}
|
||||
|
||||
|
||||
def test_Parameter():
|
||||
p1 = Parameter('description1', datatype=IntRange(), default=0)
|
||||
p2 = Parameter('description2', datatype=IntRange(), constant=1)
|
||||
assert p1 != p2
|
||||
assert p1.ctr != p2.ctr
|
||||
class Mod(HasAccessibles):
|
||||
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
||||
p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
|
||||
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):
|
||||
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():
|
||||
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)
|
||||
q = o.apply(p)
|
||||
qctr = q.ctr
|
||||
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
|
||||
class Mod(Base):
|
||||
p1 = Parameter(default=True)
|
||||
p2 = Parameter() # override without change
|
||||
|
||||
o2 = Override(default=True)
|
||||
q2 = o2.apply(p)
|
||||
assert q2.ctr == p.ctr # reorder=False: take ctr from inherited param
|
||||
assert q2 != p
|
||||
assert repr(q2) != repr(p)
|
||||
assert Mod.p1 != Base.p1
|
||||
assert Mod.p2 != Base.p2
|
||||
assert Mod.p3 == Base.p3
|
||||
|
||||
q3 = Override().apply(p) # Override without change
|
||||
assert id(q2) != id(p) # must be a new object
|
||||
assert repr(q3) == repr(p) # but must be a clone
|
||||
assert id(Mod.p2) != id(Base.p2) # must be a new object
|
||||
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
|
||||
|
||||
|
||||
def test_Parameters():
|
||||
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True)))
|
||||
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True)
|
||||
assert ps['_p2'].export == '_p2'
|
||||
def test_Export():
|
||||
class Mod:
|
||||
param = Parameter('description1', datatype=BoolType, default=False)
|
||||
assert Mod.param.export == '_param'
|
||||
|
@ -24,38 +24,58 @@
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
||||
from secop.errors import ProgrammingError, ConfigError
|
||||
from secop.properties import Property, Properties, HasProperties
|
||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||
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 = [
|
||||
[(StringType(), 'default', 'extname', False, False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)],
|
||||
[(IntRange(), '42', '_extname', False, True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
||||
[(IntRange(), '42', '_extname', True, False),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=False)],
|
||||
[(IntRange(), 42, '_extname', True, True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
||||
[(IntRange(), 0, '', True, True),
|
||||
dict(default=0, extname='', export=True, mandatory=True)],
|
||||
[(IntRange(), 0, '', True, False),
|
||||
dict(default=0, extname='', export=True, mandatory=False)],
|
||||
[(IntRange(), 0, '', False, True),
|
||||
dict(default=0, extname='', export=False, mandatory=True)],
|
||||
[(IntRange(), 0, '', False, False),
|
||||
dict(default=0, extname='', export=False, mandatory=False)],
|
||||
[(IntRange(), None, '', None),
|
||||
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
|
||||
[(ValueType(), 1, '', False),
|
||||
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
|
||||
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='name'),
|
||||
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=True, mandatory=True),
|
||||
dict(default=0, extname='', export=True, mandatory=True)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=True, mandatory=False),
|
||||
dict(default=0, extname='', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=False, mandatory=True),
|
||||
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)
|
||||
def test_Property(args, check):
|
||||
p = Property('', *args)
|
||||
@pytest.mark.parametrize('propargs, check', V_test_Property)
|
||||
def test_Property(propargs, check):
|
||||
name, args, kwds = propargs
|
||||
p = Property('', *args, **kwds)
|
||||
if name:
|
||||
p.__set_name__(None, name)
|
||||
result = {k: getattr(p, k) for k in check}
|
||||
assert result == check
|
||||
|
||||
|
||||
def test_Property_basic():
|
||||
with pytest.raises(TypeError):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
@ -67,47 +87,47 @@ def test_Property_basic():
|
||||
Property('', 1)
|
||||
Property('', IntRange(), '42', 'extname', False, False)
|
||||
|
||||
|
||||
def test_Properties():
|
||||
p = Properties()
|
||||
with pytest.raises(ProgrammingError):
|
||||
p[1] = 2
|
||||
p['a'] = Property('', IntRange(), '42', export=True)
|
||||
assert p['a'].default == 42
|
||||
assert p['a'].export is True
|
||||
assert p['a'].extname == '_a'
|
||||
with pytest.raises(ProgrammingError):
|
||||
p['a'] = 137
|
||||
with pytest.raises(ProgrammingError):
|
||||
del p[1]
|
||||
with pytest.raises(ProgrammingError):
|
||||
del p['a']
|
||||
p['a'] = Property('', IntRange(), 0, export=False)
|
||||
assert p['a'].default == 0
|
||||
assert p['a'].export is False
|
||||
assert p['a'].extname == ''
|
||||
class Cls(HasProperties):
|
||||
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||
bb = Property('', IntRange(), 0, export=False)
|
||||
|
||||
assert Cls.aa.default == 42
|
||||
assert Cls.aa.export is True
|
||||
assert Cls.aa.extname == '_aa'
|
||||
|
||||
cc = Cls()
|
||||
with pytest.raises(BadValueError):
|
||||
cc.aa = 137
|
||||
|
||||
assert Cls.bb.default == 0
|
||||
assert Cls.bb.export is False
|
||||
assert Cls.bb.extname == ''
|
||||
|
||||
|
||||
class c(HasProperties):
|
||||
properties = {
|
||||
'a' : Property('', IntRange(), 1),
|
||||
}
|
||||
# properties
|
||||
a = Property('', IntRange(), 1)
|
||||
|
||||
|
||||
class cl(c):
|
||||
properties = {
|
||||
'a' : Property('', IntRange(), 3),
|
||||
'b' : Property('', FloatRange(), 3.14),
|
||||
'minabc': Property('', IntRange(), 8),
|
||||
'maxabc': Property('', IntRange(), 9),
|
||||
'minx': Property('', IntRange(), 2),
|
||||
'maxy': Property('', IntRange(), 1),
|
||||
}
|
||||
# properties
|
||||
a = Property('', IntRange(), 3)
|
||||
b = Property('', FloatRange(), 3.14)
|
||||
minabc = Property('', IntRange(), 8)
|
||||
maxabc = Property('', IntRange(), 9)
|
||||
minx = Property('', IntRange(), 2)
|
||||
maxy = Property('', IntRange(), 1)
|
||||
|
||||
|
||||
def test_HasProperties():
|
||||
o = c()
|
||||
assert o.properties['a'] == 1
|
||||
assert o.a == 1
|
||||
o = cl()
|
||||
assert o.properties['a'] == 3
|
||||
assert o.properties['b'] == 3.14
|
||||
assert o.a == 3
|
||||
assert o.b == 3.14
|
||||
|
||||
|
||||
def test_Property_checks():
|
||||
o = c()
|
||||
@ -119,6 +139,7 @@ def test_Property_checks():
|
||||
with pytest.raises(ConfigError):
|
||||
o.checkProperties()
|
||||
|
||||
|
||||
def test_Property_override():
|
||||
o1 = c()
|
||||
class co(c):
|
||||
@ -131,10 +152,10 @@ def test_Property_override():
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
def a(self):
|
||||
pass
|
||||
assert 'collides with method' in str(e.value)
|
||||
assert 'collides with' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
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