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:
zolliker 2021-02-12 18:37:04 +01:00
parent ed02131a37
commit 1a8ddbc696
34 changed files with 1678 additions and 1978 deletions

View File

@ -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

View File

@ -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
.........

View File

@ -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

View File

@ -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):

View File

@ -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>')

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 '')

View File

@ -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,
)

View File

@ -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)

View File

@ -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:

View File

@ -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))

View File

@ -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()

View File

@ -22,6 +22,8 @@
"""Define Simulation classes"""
# TODO: rework after syntax change!
import random
from time import sleep

View File

@ -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)

View File

@ -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)

View File

@ -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])

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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(',')

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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)