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 max-returns=10
# Maximum number of branch for function / method body # Maximum number of branch for function / method body
max-branches=40 max-branches=50
# Maximum number of statements in function / method body # Maximum number of statements in function / method body
max-statements=150 max-statements=150
# Maximum number of parents for a class (see R0901). # Maximum number of parents for a class (see R0901).
max-parents=10 max-parents=15
# Maximum number of attributes for a class (see R0902). # Maximum number of attributes for a class (see R0902).
max-attributes=50 max-attributes=50

View File

@ -20,13 +20,12 @@ Parameters, Commands and Properties
................................... ...................................
.. autoclass:: secop.params.Parameter .. autoclass:: secop.params.Parameter
.. autoclass:: secop.params.usercommand .. autoclass:: secop.params.Command
.. autoclass:: secop.properties.Property .. autoclass:: secop.properties.Property
.. autoclass:: secop.modules.Attached .. autoclass:: secop.modules.Attached
:show-inheritance: :show-inheritance:
Datatypes Datatypes
......... .........

View File

@ -29,11 +29,10 @@
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \ from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached, Done
from secop.properties import Property from secop.properties import Property
from secop.params import Parameter, Command, Override, usercommand from secop.params import Parameter, Command
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
from secop.metaclass import Done
from secop.iohandler import IOHandler, IOHandlerBase from secop.iohandler import IOHandler, IOHandlerBase
from secop.stringio import StringIO, HasIodev from secop.stringio import StringIO, HasIodev
from secop.proxy import SecNode, Proxy, proxy_class from secop.proxy import SecNode, Proxy, proxy_class

View File

@ -151,11 +151,12 @@ class Stub(DataType):
""" """
for dtcls in globals().values(): for dtcls in globals().values():
if isinstance(dtcls, type) and issubclass(dtcls, DataType): if isinstance(dtcls, type) and issubclass(dtcls, DataType):
for prop in dtcls.properties.values(): for prop in dtcls.propertyDict.values():
stub = prop.datatype stub = prop.datatype
if isinstance(stub, cls): if isinstance(stub, cls):
prop.datatype = globals()[stub.name](*stub.args) prop.datatype = globals()[stub.name](*stub.args)
# SECoP types: # SECoP types:
class FloatRange(DataType): class FloatRange(DataType):
@ -165,16 +166,14 @@ class FloatRange(DataType):
:param maxval: (property **max**) :param maxval: (property **max**)
:param kwds: any of the properties below :param kwds: any of the properties below
""" """
properties = { min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max), max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max), unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''), fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'), absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0), extname='absolute_resolution', default=0.0)
extname='absolute_resolution', default=0.0), relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0), extname='relative_resolution', default=1.2e-7)
extname='relative_resolution', default=1.2e-7),
}
def __init__(self, minval=None, maxval=None, **kwds): def __init__(self, minval=None, maxval=None, **kwds):
super().__init__() super().__init__()
@ -247,12 +246,10 @@ class IntRange(DataType):
:param minval: (property **min**) :param minval: (property **min**)
:param maxval: (property **max**) :param maxval: (property **max**)
""" """
properties = { min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True), max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
# a unit on an int is now allowed in SECoP, but do we need them in Frappy? # a unit on an int is now allowed in SECoP, but do we need them in Frappy?
# 'unit': Property('physical unit', StringType(), extname='unit', default=''), # unit = Property('physical unit', StringType(), extname='unit', default='')
}
def __init__(self, minval=None, maxval=None): def __init__(self, minval=None, maxval=None):
super().__init__() super().__init__()
@ -278,7 +275,12 @@ class IntRange(DataType):
raise BadValueError('Can not convert %r to int' % value) raise BadValueError('Can not convert %r to int' % value)
def __repr__(self): def __repr__(self):
return 'IntRange(%d, %d)' % (self.min, self.max) args = (self.min, self.max)
if args[1] == DEFAULT_MAX_INT:
args = args[:1]
if args[0] == DEFAULT_MIN_INT:
args = ()
return 'IntRange%s' % repr(args)
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -315,24 +317,23 @@ class ScaledInteger(DataType):
note: limits are for the scaled float value note: limits are for the scaled float value
the scale is only used for calculating to/from transport serialisation the scale is only used for calculating to/from transport serialisation
""" """
properties = { scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True), min = Property('low limit', FloatRange(), extname='min', mandatory=True)
'min': Property('low limit', FloatRange(), extname='min', mandatory=True), max = Property('high limit', FloatRange(), extname='max', mandatory=True)
'max': Property('high limit', FloatRange(), extname='max', mandatory=True), unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''), fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'), absolute_resolution = Property('absolute resolution', FloatRange(0),
'absolute_resolution': Property('absolute resolution', FloatRange(0), extname='absolute_resolution', default=0.0)
extname='absolute_resolution', default=0.0), relative_resolution = Property('relative resolution', FloatRange(0),
'relative_resolution': Property('relative resolution', FloatRange(0), extname='relative_resolution', default=1.2e-7)
extname='relative_resolution', default=1.2e-7),
}
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds): def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
super().__init__() super().__init__()
scale = float(scale) scale = float(scale)
if absolute_resolution is None: if absolute_resolution is None:
absolute_resolution = scale absolute_resolution = scale
self.set_properties(scale=scale, self.set_properties(
scale=scale,
min=DEFAULT_MIN_INT * scale if minval is None else float(minval), min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval), max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
absolute_resolution=absolute_resolution, absolute_resolution=absolute_resolution,
@ -437,7 +438,8 @@ class EnumType(DataType):
return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)} return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)}
def __repr__(self): def __repr__(self):
return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members)) return "EnumType(%r, %s)" % (self._enum.name,
', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members))
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -460,6 +462,9 @@ class EnumType(DataType):
def format_value(self, value, unit=None): def format_value(self, value, unit=None):
return '%s<%s>' % (self._enum[value].name, self._enum[value].value) return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
def set_name(self, name):
self._enum.name = name
def compatible(self, other): def compatible(self, other):
for m in self._enum.members: for m in self._enum.members:
other(m) other(m)
@ -471,12 +476,10 @@ class BLOBType(DataType):
internally treated as bytes internally treated as bytes
""" """
properties = { minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes', default=0)
default=0), maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes', mandatory=True)
mandatory=True),
}
def __init__(self, minbytes=0, maxbytes=None): def __init__(self, minbytes=0, maxbytes=None):
super().__init__() super().__init__()
@ -538,14 +541,12 @@ class StringType(DataType):
for parameters see properties below for parameters see properties below
""" """
properties = { minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED), extname='minchars', default=0)
extname='minchars', default=0), maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED), extname='maxchars', default=UNLIMITED)
extname='maxchars', default=UNLIMITED), isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII', Stub('BoolType'), extname='isUTF8', default=False)
Stub('BoolType'), extname='isUTF8', default=False),
}
def __init__(self, minchars=0, maxchars=None, **kwds): def __init__(self, minchars=0, maxchars=None, **kwds):
super().__init__() super().__init__()
@ -611,7 +612,8 @@ class StringType(DataType):
# TextType is a special StringType intended for longer texts (i.e. embedding \n), # TextType is a special StringType intended for longer texts (i.e. embedding \n),
# whereas StringType is supposed to not contain '\n' # whereas StringType is supposed to not contain '\n'
# unfortunately, SECoP makes no distinction here.... # unfortunately, SECoP makes no distinction here....
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation # note: content is supposed to follow the format of a git commit message,
# i.e. a line of text, 2 '\n' + a longer explanation
class TextType(StringType): class TextType(StringType):
def __init__(self, maxchars=None): def __init__(self, maxchars=None):
if maxchars is None: if maxchars is None:
@ -621,7 +623,7 @@ class TextType(StringType):
def __repr__(self): def __repr__(self):
if self.maxchars == UNLIMITED: if self.maxchars == UNLIMITED:
return 'TextType()' return 'TextType()'
return 'TextType(%d)' % (self.maxchars) return 'TextType(%d)' % self.maxchars
def copy(self): def copy(self):
# DataType.copy will not work, because it is exported as 'string' # DataType.copy will not work, because it is exported as 'string'
@ -678,12 +680,10 @@ class ArrayOf(DataType):
:param members: the datatype of the elements :param members: the datatype of the elements
""" """
properties = { minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen', default=0)
default=0), maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen', mandatory=True)
mandatory=True),
}
def __init__(self, members, minlen=0, maxlen=None): def __init__(self, members, minlen=0, maxlen=None):
super().__init__() super().__init__()
@ -714,7 +714,7 @@ class ArrayOf(DataType):
def setProperty(self, key, value): def setProperty(self, key, value):
"""set also properties of members""" """set also properties of members"""
if key in self.__class__.properties: if key in self.propertyDict:
super().setProperty(key, value) super().setProperty(key, value)
else: else:
self.members.setProperty(key, value) self.members.setProperty(key, value)
@ -806,8 +806,7 @@ class TupleOf(DataType):
try: try:
if len(value) != len(self.members): if len(value) != len(self.members):
raise BadValueError( raise BadValueError(
'Illegal number of Arguments! Need %d arguments.' % 'Illegal number of Arguments! Need %d arguments.' % len(self.members))
(len(self.members)))
# validate elements and return as list # validate elements and return as list
return tuple(sub(elem) return tuple(sub(elem)
for sub, elem in zip(self.members, value)) for sub, elem in zip(self.members, value))
@ -991,8 +990,8 @@ class CommandType(DataType):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
# internally used datatypes (i.e. only for programming the SEC-node) # internally used datatypes (i.e. only for programming the SEC-node)
class DataTypeType(DataType): class DataTypeType(DataType):
def __call__(self, value): def __call__(self, value):
"""check if given value (a python obj) is a valid datatype """check if given value (a python obj) is a valid datatype
@ -1091,16 +1090,13 @@ class LimitsType(TupleOf):
class StatusType(TupleOf): class StatusType(TupleOf):
# shorten initialisation and allow acces to status enumMembers from status values # shorten initialisation and allow access to status enumMembers from status values
def __init__(self, enum): def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType()) TupleOf.__init__(self, EnumType(enum), StringType())
self.enum = enum self._enum = enum
def __getattr__(self, key): def __getattr__(self, key):
enum = TupleOf.__getattr__(self, 'enum') return getattr(self._enum, key)
if hasattr(enum, key):
return getattr(enum, key)
return TupleOf.__getattr__(self, key)
def floatargs(kwds): def floatargs(kwds):

View File

@ -24,11 +24,10 @@
from secop.datatypes import ArrayOf, BoolType, EnumType, \ from secop.datatypes import ArrayOf, BoolType, EnumType, \
FloatRange, StringType, StructOf, TupleOf FloatRange, StringType, StructOf, TupleOf
from secop.metaclass import ModuleMeta from secop.modules import Command, Parameter, HasAccessibles
from secop.modules import Command, Parameter
class Feature(metaclass=ModuleMeta): class Feature(HasAccessibles):
"""all things belonging to a small, predefined functionality influencing the working of a module""" """all things belonging to a small, predefined functionality influencing the working of a module"""
@ -39,33 +38,37 @@ class HAS_PID(Feature):
# note: (i would still but them in the same group, though) # note: (i would still but them in the same group, though)
# note: if extra elements are implemented in the pid struct they MUST BE # note: if extra elements are implemented in the pid struct they MUST BE
# properly described in the description of the pid Parameter # properly described in the description of the pid Parameter
parameters = {
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ), # parameters
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ), use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True), # pylint: disable=invalid-name
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True), p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True), i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
'pid': Parameter('(optional) Struct of p,i,d, minimum output value', d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
pid = Parameter('(optional) Struct of p,i,d, minimum output value',
datatype=StructOf(p=FloatRange(0), datatype=StructOf(p=FloatRange(0),
i=FloatRange(0), i=FloatRange(0),
d=FloatRange(0), d=FloatRange(0),
base_output=FloatRange(0), base_output=FloatRange(0),
), optional=True, ), optional=True,
), # note: struct may be extended with custom elements (names should be prefixed with '_') ) # note: struct may be extended with custom elements (names should be prefixed with '_')
'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False), output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False)
}
class Has_PIDTable(HAS_PID): class Has_PIDTable(HAS_PID):
parameters = {
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)), # parameters
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0), use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
StructOf(p=FloatRange(0), StructOf(p=FloatRange(0),
i=FloatRange(0), i=FloatRange(0),
d=FloatRange(0), d=FloatRange(0),
_heater_range=FloatRange(0), _heater_range=FloatRange(0),
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange' _base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
}
class HAS_Persistent(Feature): class HAS_Persistent(Feature):
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
# 'coupled' : Status.BUSY+2, # to be discussed. # 'coupled' : Status.BUSY+2, # to be discussed.
# 'decoupling' : Status.BUSY+3, # to be discussed. # 'decoupling' : Status.BUSY+3, # to be discussed.
#} #}
parameters = {
'persistent_mode': Parameter('Use persistent mode', # parameters
persistent_mode = Parameter('Use persistent mode',
datatype=EnumType(off=0,on=1), datatype=EnumType(off=0,on=1),
default=0, readonly=False), default=0, readonly=False)
'is_persistent': Parameter('current state of persistence', is_persistent = Parameter('current state of persistence',
datatype=BoolType(), optional=True), datatype=BoolType(), optional=True)
'stored_value': Parameter('current persistence value, often used as the modules value', stored_value = Parameter('current persistence value, often used as the modules value',
datatype='main', unit='$', optional=True), datatype='main', unit='$', optional=True)
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)', driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
datatype='main', unit='$' ), datatype='main', unit='$' )
}
class HAS_Tolerance(Feature): class HAS_Tolerance(Feature):
# detects IDLE status by checking if the value lies in a given window: # detects IDLE status by checking if the value lies in a given window:
# tolerance is the maximum allowed deviation from target, value must lie in this interval # tolerance is the maximum allowed deviation from target, value must lie in this interval
# for at least ´timewindow´ seconds. # for at least ´timewindow´ seconds.
parameters = {
'tolerance': Parameter('Half height of the Window', # parameters
datatype=FloatRange(0), default=1, unit='$'), tolerance = Parameter('Half height of the Window',
'timewindow': Parameter('Length of the timewindow to check', datatype=FloatRange(0), default=1, unit='$')
timewindow = Parameter('Length of the timewindow to check',
datatype=FloatRange(0), default=30, unit='s', datatype=FloatRange(0), default=30, unit='s',
optional=True), optional=True)
}
class HAS_Timeout(Feature): class HAS_Timeout(Feature):
parameters = {
'timeout': Parameter('timeout for movement', # parameters
datatype=FloatRange(0), default=0, unit='s'), timeout = Parameter('timeout for movement',
} datatype=FloatRange(0), default=0, unit='s')
class HAS_Pause(Feature): class HAS_Pause(Feature):
# just a proposal, can't agree on it.... # just a proposal, can't agree on it....
parameters = {
'pause': Command('pauses movement', argument=None, result=None), @Command(argument=None, result=None)
'go': Command('continues movement or start a new one if target was change since the last pause', def pause(self):
argument=None, result=None), """pauses movement"""
}
@Command(argument=None, result=None)
def go(self):
"""continues movement or start a new one if target was change since the last pause"""
class HAS_Ramp(Feature): class HAS_Ramp(Feature):
parameters = {
'ramp': Parameter('speed of movement', unit='$/min', # parameters
datatype=FloatRange(0)), ramp =Parameter('speed of movement', unit='$/min',
'use_ramp': Parameter('use the ramping of the setpoint, or jump', datatype=FloatRange(0))
use_ramp = Parameter('use the ramping of the setpoint, or jump',
datatype=EnumType(disable_ramp=0, use_ramp=1), datatype=EnumType(disable_ramp=0, use_ramp=1),
optional=True), optional=True)
'setpoint': Parameter('currently active setpoint', setpoint = Parameter('currently active setpoint',
datatype=FloatRange(0), unit='$', datatype=FloatRange(0), unit='$',
readonly=True, ), readonly=True, )
}
class HAS_Speed(Feature): class HAS_Speed(Feature):
parameters = {
'speed' : Parameter('(maximum) speed of movement (of the main value)', # parameters
unit='$/s', datatype=FloatRange(0)), speed = Parameter('(maximum) speed of movement (of the main value)',
} unit='$/s', datatype=FloatRange(0))
class HAS_Accel(HAS_Speed): class HAS_Accel(HAS_Speed):
parameters = {
'accel' : Parameter('acceleration of movement', unit='$/s^2', # parameters
datatype=FloatRange(0)), accel = Parameter('acceleration of movement', unit='$/s^2',
'decel' : Parameter('deceleration of movement', unit='$/s^2', datatype=FloatRange(0))
datatype=FloatRange(0), optional=True), decel = Parameter('deceleration of movement', unit='$/s^2',
} datatype=FloatRange(0), optional=True)
class HAS_MotorCurrents(Feature): class HAS_MotorCurrents(Feature):
parameters = {
'movecurrent' : Parameter('Current while moving', # parameters
datatype=FloatRange(0)), movecurrent = Parameter('Current while moving',
'idlecurrent' : Parameter('Current while idle', datatype=FloatRange(0))
datatype=FloatRange(0), optional=True), idlecurrent = Parameter('Current while idle',
} datatype=FloatRange(0), optional=True)
class HAS_Curve(Feature): class HAS_Curve(Feature):
# proposed, not yet agreed upon! # proposed, not yet agreed upon!
parameters = {
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'), # parameters
# XXX: tbd. (how to upload/download/select a curve?) curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
}

View File

@ -54,7 +54,7 @@ method has to be called explicitly int the write_<parameter> method, if needed.
""" """
import re import re
from secop.metaclass import Done from secop.modules import Done
from secop.errors import ProgrammingError from secop.errors import ProgrammingError

View File

@ -67,6 +67,7 @@ SIMPLETYPES = {
'IntRange': 'int', 'IntRange': 'int',
'BlobType': 'bytes', 'BlobType': 'bytes',
'StringType': 'str', 'StringType': 'str',
'TextType': 'str',
'BoolType': 'bool', 'BoolType': 'bool',
'StructOf': 'dict', 'StructOf': 'dict',
} }
@ -179,7 +180,7 @@ def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
def class_doc_handler(app, what, name, cls, options, lines): def class_doc_handler(app, what, name, cls, options, lines):
if what == 'class': if what == 'class':
if issubclass(cls, HasProperties): if issubclass(cls, HasProperties):
append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property) append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property)
if issubclass(cls, Module): if issubclass(cls, Module):
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param) append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command) append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)

View File

@ -141,7 +141,7 @@ class SequencerMixin:
return self.read_hw_status() return self.read_hw_status()
return self.Status.IDLE, '' return self.Status.IDLE, ''
def do_stop(self): def stop(self):
if self.seq_is_alive(): if self.seq_is_alive():
self._seq_stopflag = True self._seq_stopflag = True

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> # Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""Define Baseclasses for real Modules implemented in the server""" """Define base classes for real Modules implemented in the server"""
import sys import sys
import time import time
from collections import OrderedDict
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \ from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
@ -33,19 +32,147 @@ from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueErro
SilentError, InternalError, secop_error SilentError, InternalError, secop_error
from secop.lib import formatException, formatExtendedStack, mkthread from secop.lib import formatException, formatExtendedStack, mkthread
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.metaclass import ModuleMeta from secop.params import PREDEFINED_ACCESSIBLES, Command, Parameter, Accessible
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
from secop.poller import Poller, BasicPoller from secop.poller import Poller, BasicPoller
# XXX: connect with 'protocol'-Modules. Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
# Idea: every Module defined herein is also a 'protocol'-Module,
# all others MUST derive from those, the 'interface'-class is still derived
# from these base classes (how to do this?)
class Module(HasProperties, metaclass=ModuleMeta): class HasAccessibles(HasProperties):
"""base class of Module
joining the class's properties, parameters and commands dicts with
those of base classes.
wrap read_*/write_* methods
(so the dispatcher will get notified of changed values)
"""
@classmethod
def __init_subclass__(cls): # pylint: disable=too-many-branches
super().__init_subclass__()
# merge accessibles from all sub-classes, treat overrides
# for now, allow to use also the old syntax (parameters/commands dict)
accessibles = {}
for base in cls.__bases__:
accessibles.update(getattr(base, 'accessibles', {}))
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
for aname, aobj in accessibles.items():
value = getattr(cls, aname, None)
if not isinstance(value, Accessible): # else override is already done in __set_name__
anew = aobj.override(value)
newaccessibles[aname] = anew
setattr(cls, aname, anew)
anew.__set_name__(cls, aname)
ordered = {}
for aname in cls.__dict__.get('paramOrder', ()):
if aname in accessibles:
ordered[aname] = accessibles.pop(aname)
elif aname in newaccessibles:
ordered[aname] = newaccessibles.pop(aname)
# ignore unknown names
# starting from old accessibles not mentioned, append items from 'order'
accessibles.update(ordered)
# then new accessibles not mentioned
accessibles.update(newaccessibles)
cls.accessibles = accessibles
# Correct naming of EnumTypes
for k, v in accessibles.items():
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
v.datatype.set_name(k)
# check validity of Parameter entries
for pname, pobj in accessibles.items():
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
if isinstance(pobj, Command):
# nothing to do for now
continue
rfunc = cls.__dict__.get('read_' + pname, None)
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
if rfunc_handler:
if rfunc:
raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname))
rfunc = rfunc_handler
# create wrapper except when read function is already wrapped
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("calling %r" % rfunc)
try:
value = rfunc(self)
self.log.debug("rfunc(%s) returned %r" % (pname, value))
if value is Done: # the setter is already triggered
return getattr(self, pname)
except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e))
self.announceUpdate(pname, None, e)
raise
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.accessibles[pname].value
setattr(self, pname, value) # important! trigger the setter
return value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(cls, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = getattr(cls, 'write_' + pname, None)
if wfunc is None: # ignore the handler, if a write function is present
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
# create wrapper except when write function is already wrapped
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("check validity of %s = %r" % (pname, value))
pobj = self.accessibles[pname]
value = pobj.datatype(value)
if wfunc:
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
returned_value = wfunc(self, value)
if returned_value is Done: # the setter is already triggered
return getattr(self, pname)
if returned_value is not None: # goodie: accept missing return value
value = returned_value
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(cls, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
# check information about Command's
for attrname in cls.__dict__:
if attrname.startswith('do_'):
raise ProgrammingError('%r: old style command %r not supported anymore'
% (cls.__name__, attrname))
res = {}
# collect info about properties
for pn, pv in cls.propertyDict.items():
if pv.settable:
res[pn] = pv
# collect info about parameters and their properties
for param, pobj in cls.accessibles.items():
res[param] = {}
for pn, pv in pobj.getProperties().items():
if pv.settable:
res[param][pn] = pv
cls.configurables = res
class Module(HasAccessibles):
"""basic module """basic module
all SECoP modules derive from this. all SECoP modules derive from this.
@ -58,7 +185,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
Notes: Notes:
- the programmer normally should not need to reimplement :meth:`__init__` - the programmer normally should not need to reimplement :meth:`__init__`
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc... - within modules, parameters should only be addressed as ``self.<pname>``,
i.e. ``self.value``, ``self.target`` etc...
- these are accessing the cached version. - these are accessing the cached version.
- they can also be written to, generating an async update - they can also be written to, generating an async update
@ -77,22 +205,17 @@ class Module(HasProperties, metaclass=ModuleMeta):
# note: properties don't change after startup and are usually filled # note: properties don't change after startup and are usually filled
# with data from a cfg file... # with data from a cfg file...
# note: only the properties predefined here are allowed to be set in the cfg file # note: only the properties predefined here are allowed to be set in the cfg file
# note: the names map to a [datatype, value] list, value comes from the cfg file, export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
# datatype is fixed! group = Property('optional group the module belongs to', StringType(), default='', extname='group')
properties = { description = Property('description of the module', TextType(), extname='description', mandatory=True)
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False), meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'), default=('', 0), extname='meaning')
'description': Property('Description of the module', TextType(), extname='description', mandatory=True), visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)), default='user', extname='visibility')
default=('',0), extname='meaning'), implementation = Property('internal name of the implementation class of the module', StringType(),
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='implementation')
default='user', extname='visibility'), interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
'implementation': Property('Internal name of the implementation class of the module', StringType(), extname='interface_classes')
extname='implementation'),
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
extname='interface_classes'),
# what else?
}
# properties, parameters and commands are auto-merged upon subclassing # properties, parameters and commands are auto-merged upon subclassing
parameters = {} parameters = {}
@ -113,14 +236,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
# handle module properties # handle module properties
# 1) make local copies of properties # 1) make local copies of properties
super(Module, self).__init__() super().__init__()
# 2) check and apply properties specified in cfgdict # 2) check and apply properties specified in cfgdict
# specified as '.<propertyname> = <propertyvalue>' # specified as '.<propertyname> = <propertyvalue>'
# (this is for legacy config files only) # (this is for legacy config files only)
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if k[0] == '.': if k[0] == '.':
if k[1:] in self.__class__.properties: if k[1:] in self.propertyDict:
self.setProperty(k[1:], cfgdict.pop(k)) self.setProperty(k[1:], cfgdict.pop(k))
else: else:
raise ConfigError('Module %r has no property %r' % raise ConfigError('Module %r has no property %r' %
@ -128,20 +251,20 @@ class Module(HasProperties, metaclass=ModuleMeta):
# 3) check and apply properties specified in cfgdict as # 3) check and apply properties specified in cfgdict as
# '<propertyname> = <propertyvalue>' (without '.' prefix) # '<propertyname> = <propertyvalue>' (without '.' prefix)
for k in self.__class__.properties: for k in self.propertyDict:
if k in cfgdict: if k in cfgdict:
self.setProperty(k, cfgdict.pop(k)) self.setProperty(k, cfgdict.pop(k))
# 4) set automatic properties # 4) set automatic properties
mycls = self.__class__ mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.properties['implementation'] = myclassname self.implementation = myclassname
# list of all 'secop' modules # list of all 'secop' modules
self.properties['interface_classes'] = [ # self.interface_classes = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')] # b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
# list of only the 'highest' secop module class # list of only the 'highest' secop module class
self.properties['interface_classes'] = [[ self.interface_classes = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]] b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
# handle Features # handle Features
# XXX: todo # XXX: todo
@ -150,7 +273,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
# 1) make local copies of parameter objects # 1) make local copies of parameter objects
# they need to be individual per instance since we use them also # they need to be individual per instance since we use them also
# to cache the current value + qualifiers... # to cache the current value + qualifiers...
accessibles = OrderedDict() accessibles = {}
# conversion from exported names to internal attribute names # conversion from exported names to internal attribute names
accessiblename2attr = {} accessiblename2attr = {}
for aname, aobj in self.accessibles.items(): for aname, aobj in self.accessibles.items():
@ -159,31 +282,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
if isinstance(aobj, Parameter): if isinstance(aobj, Parameter):
# fix default properties poll and needscfg # fix default properties poll and needscfg
if aobj.poll is None: if aobj.poll is None:
aobj.properties['poll'] = bool(aobj.handler) aobj.poll = bool(aobj.handler)
if aobj.needscfg is None: if aobj.needscfg is None:
aobj.properties['needscfg'] = not aobj.poll aobj.needscfg = not aobj.poll
if not self.export: # do not export parameters of a module not exported if not self.export: # do not export parameters of a module not exported
aobj.properties['export'] = False aobj.export = False
if aobj.export: if aobj.export:
if aobj.export is True: if aobj.export is True:
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None) predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
if predefined_obj: if predefined_obj:
if isinstance(aobj, predefined_obj): if isinstance(aobj, predefined_obj):
aobj.setProperty('export', aname) aobj.export = aname
else: else:
raise ProgrammingError("can not use '%s' as name of a %s" % raise ProgrammingError("can not use '%s' as name of a %s" %
(aname, aobj.__class__.__name__)) (aname, aobj.__class__.__name__))
else: # create custom parameter else: # create custom parameter
aobj.setProperty('export', '_' + aname) aobj.export = '_' + aname
accessiblename2attr[aobj.export] = aname accessiblename2attr[aobj.export] = aname
accessibles[aname] = aobj accessibles[aname] = aobj
# do not re-use self.accessibles as this is the same for all instances # do not re-use self.accessibles as this is the same for all instances
self.accessibles = accessibles self.accessibles = accessibles
self.accessiblename2attr = accessiblename2attr self.accessiblename2attr = accessiblename2attr
# provide properties to 'filter' out the parameters/commands # provide properties to 'filter' out the parameters/commands
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter)) self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command)) self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
# 2) check and apply parameter_properties # 2) check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>' # specified as '<paramname>.<propertyname> = <propertyvalue>'
@ -200,6 +323,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
else: else:
raise ConfigError('Module %s: Parameter %r has no property %r!' % raise ConfigError('Module %s: Parameter %r has no property %r!' %
(self.name, paramname, propname)) (self.name, paramname, propname))
else:
raise ConfigError('Module %s has no Parameter %r!' %
(self.name, paramname))
# 3) check config for problems: # 3) check config for problems:
# only accept remaining config items specified in parameters # only accept remaining config items specified in parameters
@ -209,7 +335,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
'Module %s:config Parameter %r ' 'Module %s:config Parameter %r '
'not understood! (use one of %s)' % 'not understood! (use one of %s)' %
(self.name, k, ', '.join(list(self.parameters) + (self.name, k, ', '.join(list(self.parameters) +
list(self.__class__.properties)))) list(self.propertyDict))))
# 4) complain if a Parameter entry has no default value and # 4) complain if a Parameter entry has no default value and
# is not specified in cfgdict and deal with parameters to be written. # is not specified in cfgdict and deal with parameters to be written.
@ -348,7 +474,6 @@ class Module(HasProperties, metaclass=ModuleMeta):
modobj.announceUpdate(p, value) modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None): def isBusy(self, status=None):
"""helper function for treating substates of BUSY correctly""" """helper function for treating substates of BUSY correctly"""
# defined even for non drivable (used for dynamic polling) # defined even for non drivable (used for dynamic polling)
@ -403,7 +528,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
class Readable(Module): class Readable(Module):
"""basic readable Module""" """basic readable module"""
# pylint: disable=invalid-name # pylint: disable=invalid-name
Status = Enum('Status', Status = Enum('Status',
IDLE=100, IDLE=100,
@ -413,21 +538,12 @@ class Readable(Module):
DISABLED=0, DISABLED=0,
UNKNOWN=401, UNKNOWN=401,
) #: status codes ) #: status codes
parameters = {
'value': Parameter('current value of the Module', readonly=True, value = Parameter('current value of the module', FloatRange(), poll=True)
datatype=FloatRange(), status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
poll=True, default=(Status.IDLE, ''), poll=True)
), pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
'pollinterval': Parameter('sleeptime between polls', default=5, default=5, readonly=False)
readonly=False,
datatype=FloatRange(0.1, 120),
),
'status': Parameter('current status of the Module',
default=(Status.IDLE, ''),
datatype=TupleOf(EnumType(Status), StringType()),
readonly=True, poll=True,
),
}
def startModule(self, started_callback): def startModule(self, started_callback):
"""start basic polling thread""" """start basic polling thread"""
@ -476,11 +592,9 @@ class Readable(Module):
class Writable(Readable): class Writable(Readable):
"""basic writable module""" """basic writable module"""
parameters = {
'target': Parameter('target value of the Module', target = Parameter('target value of the module',
default=0, readonly=False, datatype=FloatRange(), default=0, readonly=False, datatype=FloatRange())
),
}
class Drivable(Writable): class Drivable(Writable):
@ -488,17 +602,7 @@ class Drivable(Writable):
Status = Enum(Readable.Status, BUSY=300) #: status codes Status = Enum(Readable.Status, BUSY=300) #: status codes
commands = { status = Parameter(datatype=StatusType(Status)) # override Readable.status
'stop': Command(
'cease driving, go to IDLE state',
argument=None,
result=None
),
}
overrides = {
'status': Override(datatype=StatusType(Status)),
}
def isBusy(self, status=None): def isBusy(self, status=None):
"""check for busy, treating substates correctly """check for busy, treating substates correctly
@ -532,23 +636,16 @@ class Drivable(Writable):
self.pollOneParam(pname) self.pollOneParam(pname)
return fastpoll return fastpoll
def do_stop(self): @Command(None, result=None)
"""default implementation of the stop command def stop(self):
"""cease driving, go to IDLE state"""
by default does nothing."""
class Communicator(Module): class Communicator(Module):
"""basic abstract communication module""" """basic abstract communication module"""
commands = { @Command(StringType(), result=StringType())
"communicate": Command("provides the simplest mean to communication", def communicate(self, command):
argument=StringType(),
result=StringType()
),
}
def do_communicate(self, command):
"""communicate command """communicate command
:param command: the command to be sent :param command: the command to be sent
@ -569,7 +666,8 @@ class Attached(Property):
# we can not put this to properties.py, as it needs datatypes # we can not put this to properties.py, as it needs datatypes
def __init__(self, attrname=None): def __init__(self, attrname=None):
self.attrname = attrname self.attrname = attrname
super().__init__('attached module', StringType()) # we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
super().__init__('attached module', StringType(), mandatory=False)
def __repr__(self): def __repr__(self):
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '') return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')

View File

@ -24,62 +24,69 @@
import inspect import inspect
import itertools
from collections import OrderedDict
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \ from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
NoneOr, TextType, IntRange, TupleOf NoneOr, TextType, IntRange, TupleOf, StructOf
from secop.errors import ProgrammingError, BadValueError from secop.errors import ProgrammingError, BadValueError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
object_counter = itertools.count(1) UNSET = object() # an argument not given, not even None
class Accessible(HasProperties): class Accessible(HasProperties):
"""base class for Parameter and Command""" """base class for Parameter and Command"""
properties = {}
kwds = None # is a dict if it might be used as Override kwds = None # is a dict if it might be used as Override
def __init__(self, ctr, **kwds): def __init__(self, **kwds):
self.ctr = ctr or next(object_counter) super().__init__()
super(Accessible, self).__init__() self.init(kwds)
# do not use self.properties.update here, as no invalid values should be
def init(self, kwds):
# do not use self.propertyValues.update here, as no invalid values should be
# assigned to properties, even not before checkProperties # assigned to properties, even not before checkProperties
for k, v in kwds.items(): for k, v in kwds.items():
self.setProperty(k, v) self.setProperty(k, v)
def __repr__(self): def inherit(self, cls, owner):
props = [] for base in owner.__bases__:
for k, prop in sorted(self.__class__.properties.items()): if hasattr(base, self.name):
v = self.properties.get(k, prop.default) aobj = getattr(base, 'accessibles', {}).get(self.name)
if v != prop.default: if aobj:
props.append('%s=%r' % (k, v)) if not isinstance(aobj, cls):
return '%s(%s, ctr=%d)' % (self.__class__.__name__, ', '.join(props), self.ctr) raise ProgrammingError('%s %s.%s can not inherit from a %s' %
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
# inherit from aobj
for pname, value in aobj.propertyValues.items():
if pname not in self.propertyValues:
self.propertyValues[pname] = value
break
def as_dict(self): def as_dict(self):
return self.properties return self.propertyValues
def override(self, from_object=None, **kwds): def override(self, value=UNSET, **kwds):
"""return a copy of ourselfs, modified by <other>""" """return a copy, overridden by a bare attribute
props = dict(self.properties, ctr=self.ctr)
if from_object: and/or some properties"""
props.update(from_object.kwds) raise NotImplementedError
props.update(kwds)
props['datatype'] = props['datatype'].copy()
return type(self)(inherit=False, internally_called=True, **props)
def copy(self): def copy(self):
"""return a copy of ourselfs""" """return a (deep) copy of ourselfs"""
props = dict(self.properties, ctr=self.ctr) raise NotImplementedError
# deep copy, as datatype might be altered from config
props['datatype'] = props['datatype'].copy()
return type(self)(inherit=False, internally_called=True, **props)
def for_export(self): def for_export(self):
"""prepare for serialisation""" """prepare for serialisation"""
return self.exportProperties() raise NotImplementedError
def __repr__(self):
props = []
for k, prop in sorted(self.propertyDict.items()):
v = self.propertyValues.get(k, prop.default)
if v != prop.default:
props.append('%s=%r' % (k, v))
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
class Parameter(Accessible): class Parameter(Accessible):
@ -87,40 +94,43 @@ class Parameter(Accessible):
:param description: description :param description: description
:param datatype: the datatype :param datatype: the datatype
:param inherit: whether properties not given should be inherited. :param inherit: whether properties not given should be inherited
defaults to True when datatype or description is missing, else to False
:param reorder: when True, put this parameter after all inherited items in the accessible list
:param kwds: optional properties :param kwds: optional properties
:param ctr: (for internal use only)
:param internally_used: (for internal use only)
""" """
# storage for Parameter settings + value + qualifiers # storage for Parameter settings + value + qualifiers
properties = { description = Property(
'description': Property('mandatory description of the parameter', TextType(), 'mandatory description of the parameter', TextType(),
extname='description', mandatory=True), extname='description', mandatory=True)
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(), datatype = Property(
extname='datainfo', mandatory=True), 'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
'readonly': Property('not changeable via SECoP (default True)', BoolType(), extname='datainfo', mandatory=True)
extname='readonly', default=True), readonly = Property(
'group': Property('optional parameter group this parameter belongs to', StringType(), 'not changeable via SECoP (default True)', BoolType(),
extname='group', default=''), extname='readonly', default=True, export='always')
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), group = Property(
extname='visibility', default=1), 'optional parameter group this parameter belongs to', StringType(),
'constant': Property('optional constant value for constant parameters', ValueType(), extname='group', default='')
extname='constant', default=None, mandatory=False), visibility = Property(
'default': Property('[internal] default (startup) value of this parameter ' 'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'if it can not be read from the hardware', extname='visibility', default=1)
ValueType(), export=False, default=None, mandatory=False), constant = Property(
'export': Property(''' 'optional constant value for constant parameters', ValueType(),
[internal] export settings extname='constant', default=None)
default = Property(
'''[internal] default (startup) value of this parameter
if it can not be read from the hardware''', ValueType(),
export=False, default=None)
export = Property(
'''[internal] export settings
* False: not accessible via SECoP. * False: not accessible via SECoP.
* True: exported, name automatic. * True: exported, name automatic.
* a string: exported with custom name''', * a string: exported with custom name''', OrType(BoolType(), StringType()),
OrType(BoolType(), StringType()), export=False, default=True), export=False, default=True)
'poll': Property(''' poll = Property(
[internal] polling indicator '''[internal] polling indicator
may be: may be:
@ -132,20 +142,30 @@ class Parameter(Accessible):
* 3 (REGULAR), polled with pollperiod * 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, * 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod else polled with pollperiod
''', ''', NoneOr(IntRange()),
NoneOr(IntRange()), export=False, default=None), export=False, default=None)
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None), needscfg = Property(
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False, '[internal] needs value in config', NoneOr(BoolType()),
settable=False, default=False), export=False, default=None)
'handler': Property('[internal] overload the standard read and write functions', optional = Property(
ValueType(), export=False, default=None, mandatory=False, settable=False), '[internal] is this parameter optional?', BoolType(),
'initwrite': Property('[internal] write this parameter on initialization' export=False, settable=False, default=False)
' (default None: write if given in config)', handler = Property(
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False), '[internal] overload the standard read and write functions', ValueType(),
} export=False, default=None, settable=False)
initwrite = Property(
'''[internal] write this parameter on initialization
def __init__(self, description=None, datatype=None, inherit=True, *, default None: write if given in config''', NoneOr(BoolType()),
reorder=False, ctr=None, internally_called=False, **kwds): export=False, default=None, settable=False)
# used on the instance copy only
value = None
timestamp = 0
readerror = None
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
super().__init__(**kwds)
if datatype is not None: if datatype is not None:
if not isinstance(datatype, DataType): if not isinstance(datatype, DataType):
if isinstance(datatype, type) and issubclass(datatype, DataType): if isinstance(datatype, type) and issubclass(datatype, DataType):
@ -154,57 +174,92 @@ class Parameter(Accessible):
else: else:
raise ProgrammingError( raise ProgrammingError(
'datatype MUST be derived from class DataType!') 'datatype MUST be derived from class DataType!')
kwds['datatype'] = datatype self.datatype = datatype
if 'default' in kwds:
self.default = datatype(kwds['default'])
if description is not None: if description is not None:
if not internally_called: self.description = inspect.cleandoc(description)
description = inspect.cleandoc(description)
kwds['description'] = description
unit = kwds.pop('unit', None) # save for __set_name__
if unit is not None and datatype: # for legacy code only self._inherit = inherit
datatype.setProperty('unit', unit) self._unit = unit # for legacy code only
self._constant = constant
constant = kwds.get('constant') def __get__(self, instance, owner):
if constant is not None: # not used yet
constant = datatype(constant) if instance is None:
return self
return instance.parameters[self.name].value
def __set__(self, obj, value):
obj.announceUpdate(self.name, value)
def __set_name__(self, owner, name):
self.name = name
if self._inherit:
self.inherit(Parameter, owner)
# check for completeness
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
if missing_properties:
raise ProgrammingError('Parameter %s.%s needs a %s' %
(owner.__name__, name, ' and a '.join(missing_properties)))
if self._unit is not None:
self.datatype.setProperty('unit', self._unit)
if self._constant is not None:
constant = self.datatype(self._constant)
# The value of the `constant` property should be the # The value of the `constant` property should be the
# serialised version of the constant, or unset # serialised version of the constant, or unset
kwds['constant'] = datatype.export_value(constant) self.constant = self.datatype.export_value(constant)
kwds['readonly'] = True self.readonly = True
if internally_called: # fixes in case datatype has changed
default = kwds.get('default') if 'default' in self.propertyValues:
if default is not None: # fixes in case datatype has changed
try: try:
datatype(default) self.datatype(self.default)
except BadValueError: except BadValueError:
# clear default, if it does not match datatype # clear default, if it does not match datatype
kwds['default'] = None self.propertyValues.pop('default')
super().__init__(ctr, **kwds)
if inherit:
if reorder:
kwds['ctr'] = next(object_counter)
if unit is not None:
kwds['unit'] = unit
self.kwds = kwds # contains only the items which must be overwritten
# internal caching: value and timestamp of last change... if self.export is True:
self.value = self.default if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
self.timestamp = 0 self.export = name
self.readerror = None # if not None, indicates that last read was not successful else:
self.export = '_' + name
def copy(self):
# deep copy, as datatype might be altered from config
res = Parameter()
res.name = self.name
res.init(self.propertyValues)
res.datatype = res.datatype.copy()
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.value = res.datatype(value)
return res
def export_value(self): def export_value(self):
return self.datatype.export_value(self.value) return self.datatype.export_value(self.value)
def for_export(self):
return dict(self.exportProperties(), readonly=self.readonly)
def getProperties(self): def getProperties(self):
"""get also properties of datatype""" """get also properties of datatype"""
superProp = super().getProperties().copy() super_prop = super().getProperties().copy()
superProp.update(self.datatype.getProperties()) super_prop.update(self.datatype.getProperties())
return superProp return super_prop
def setProperty(self, key, value): def setProperty(self, key, value):
"""set also properties of datatype""" """set also properties of datatype"""
if key in self.__class__.properties: if key in self.propertyDict:
super().setProperty(key, value) super().setProperty(key, value)
else: else:
self.datatype.setProperty(key, value) self.datatype.setProperty(key, value)
@ -213,190 +268,150 @@ class Parameter(Accessible):
super().checkProperties() super().checkProperties()
self.datatype.checkProperties() self.datatype.checkProperties()
def for_export(self):
"""prepare for serialisation
readonly is mandatory for serialisation, but not for declaration in classes
"""
r = super().for_export()
if 'readonly' not in r:
r['readonly'] = self.__class__.properties['readonly'].default
return r
class UnusedClass:
# do not derive anything from this!
pass
class Parameters(OrderedDict):
"""class storage for Parameters"""
def __init__(self, *args, **kwds):
self.exported = {} # only for lookups!
super(Parameters, self).__init__(*args, **kwds)
def __setitem__(self, key, value):
if value.export:
if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)):
value.properties['export'] = key
else:
value.properties['export'] = '_' + key
self.exported[value.export] = key
super(Parameters, self).__setitem__(key, value)
def __getitem__(self, item):
return super(Parameters, self).__getitem__(self.exported.get(item, item))
class Commands(Parameters):
"""class storage for Commands"""
class Override:
"""Stores the overrides to be applied to a Parameter
note: overrides are applied by the metaclass during class creating
reorder=True: use position of Override instead of inherited for the order
"""
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
self.kwds = kwds
# allow to override description and datatype without keyword
if description:
self.kwds['description'] = description
if datatype is not None:
self.kwds['datatype'] = datatype
if reorder: # result from apply must use new ctr from Override
self.kwds['ctr'] = next(object_counter)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
def apply(self, obj):
return obj.override(self)
class Command(Accessible): class Command(Accessible):
# to be merged with usercommand
properties = {
'description': Property('description of the Command', TextType(),
extname='description', export=True, mandatory=True),
'group': Property('optional command group of the command.', StringType(),
extname='group', export=True, default=''),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1),
'export': Property('''
[internal] export settings
* False: not accessible via SECoP.
* True: exported, name automatic.
* a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
BoolType(), export=False, default=False, settable=False),
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
DataTypeType(), extname='datainfo', mandatory=True),
'argument': Property('datatype of the argument to the command, or None',
NoneOr(DataTypeType()), export=False, mandatory=True),
'result': Property('datatype of the result from the command, or None',
NoneOr(DataTypeType()), export=False, mandatory=True),
}
def __init__(self, description=None, *, reorder=False, inherit=True,
internally_called=False, ctr=None, **kwds):
if internally_called:
inherit = False
# make sure either all or no datatype info is in kwds
if 'argument' in kwds or 'result' in kwds:
datatype = CommandType(kwds.get('argument'), kwds.get('result'))
else:
datatype = kwds.get('datatype')
datainfo = {}
datainfo['datatype'] = datatype or CommandType()
datainfo['argument'] = datainfo['datatype'].argument
datainfo['result'] = datainfo['datatype'].result
if datatype:
kwds.update(datainfo)
if description is not None:
kwds['description'] = description
if datatype:
datainfo = {}
super(Command, self).__init__(ctr, **datainfo, **kwds)
if inherit:
if reorder:
kwds['ctr'] = next(object_counter)
self.kwds = kwds
@property
def argument(self):
return self.datatype.argument
@property
def result(self):
return self.datatype.result
class usercommand(Command):
"""decorator to turn a method into a command """decorator to turn a method into a command
:param argument: the datatype of the argument or None :param argument: the datatype of the argument or None
:param result: the datatype of the result or None :param result: the datatype of the result or None
:param inherit: whether properties not given should be inherited. :param inherit: whether properties not given should be inherited
defaults to True when datatype or description is missing, else to False
:param reorder: when True, put this command after all inherited items in the accessible list
:param kwds: optional properties :param kwds: optional properties
{all properties}
""" """
description = Property(
'description of the Command', TextType(),
extname='description', export=True, mandatory=True)
group = Property(
'optional command group of the command.', StringType(),
extname='group', export=True, default='')
visibility = Property(
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1)
export = Property(
'''[internal] export settings
* False: not accessible via SECoP.
* True: exported, name automatic.
* a string: exported with custom name''', OrType(BoolType(), StringType()),
export=False, default=True)
optional = Property(
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
export=False, default=False, settable=False)
datatype = Property(
"datatype of the command, auto generated from 'argument' and 'result'",
DataTypeType(), extname='datainfo', export='always')
argument = Property(
'datatype of the argument to the command, or None', NoneOr(DataTypeType()),
export=False, mandatory=True)
result = Property(
'datatype of the result from the command, or None', NoneOr(DataTypeType()),
export=False, mandatory=True)
func = None func = None
def __init__(self, argument=False, result=None, inherit=True, **kwds): def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
super().__init__(**kwds)
if result or kwds or isinstance(argument, DataType) or not callable(argument): if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case # normal case
self.func = None
if argument is False and result: if argument is False and result:
argument = None argument = None
if argument is not False: if argument is not False:
if isinstance(argument, (tuple, list)): if isinstance(argument, (tuple, list)):
# goodie: allow declaring multiple arguments as a tuple # goodie: treat as TupleOf
# TODO: check that calling works properly
argument = TupleOf(*argument) argument = TupleOf(*argument)
kwds['argument'] = argument self.argument = argument
kwds['result'] = result self.result = result
self.kwds = kwds
else: else:
# goodie: allow @usercommand instead of @usercommand() # goodie: allow @Command instead of @Command()
self.func = argument # this is the wrapped method! self.func = argument # this is the wrapped method!
if argument.__doc__ is not None: if argument.__doc__:
kwds['description'] = argument.__doc__ self.description = inspect.cleandoc(argument.__doc__)
self.name = self.func.__name__ self.name = self.func.__name__
super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds) self._inherit = inherit # save for __set_name__
def override(self, from_object=None, **kwds):
result = super().override(from_object, **kwds)
func = kwds.pop('func', from_object.func if from_object else None)
if func:
result(func) # pylint: disable=not-callable
return result
def __set_name__(self, owner, name): def __set_name__(self, owner, name):
self.name = name self.name = name
if self.func is None:
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
(owner.__name__, name))
if self._inherit:
self.inherit(Command, owner)
self.datatype = CommandType(self.argument, self.result)
if self.export is True:
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
self.export = name
else:
self.export = '_' + name
def __get__(self, obj, owner=None): def __get__(self, obj, owner=None):
if obj is None: if obj is None:
return self return self
if not self.func: if not self.func:
raise ProgrammingError('usercommand %s not properly configured' % self.name) raise ProgrammingError('Command %s not properly configured' % self.name)
return self.func.__get__(obj, owner) return self.func.__get__(obj, owner)
def __call__(self, fun): def __call__(self, func):
description = self.kwds.get('description') or fun.__doc__ if 'description' not in self.propertyValues and func.__doc__:
self.properties['description'] = self.kwds['description'] = description self.description = inspect.cleandoc(func.__doc__)
self.name = fun.__name__ self.func = func
self.func = fun
return self return self
def copy(self):
res = Command()
res.name = self.name
res.func = self.func
res.init(self.propertyValues)
if res.argument:
res.argument = res.argument.copy()
if res.result:
res.result = res.result.copy()
res.datatype = CommandType(res.argument, res.result)
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.func = value
return res
def do(self, module_obj, argument):
"""perform function call
:param module_obj: the module on which the command is to be executed
:param argument: the argument from the do command
:returns: the return value converted to the result type
- when the argument type is TupleOf, the function is called with multiple arguments
- when the argument type is StructOf, the function is called with keyworded arguments
- the validity of the argument/s is/are checked
"""
func = self.__get__(module_obj)
if self.argument:
# validate
argument = self.argument(argument)
if isinstance(self.argument, TupleOf):
res = func(*argument)
elif isinstance(self.argument, StructOf):
res = func(**argument)
else:
res = func(argument)
else:
if argument is not None:
raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name))
res = func()
if self.result:
return self.result(res)
return None # silently ignore the result from the method
def for_export(self):
return self.exportProperties()
def __repr__(self):
result = super().__repr__()
return result[:-1] + ', %r)' % self.func if self.func else result
# list of predefined accessibles with their type # list of predefined accessibles with their type
PREDEFINED_ACCESSIBLES = dict( PREDEFINED_ACCESSIBLES = dict(

View File

@ -23,27 +23,44 @@
"""Define validated data types.""" """Define validated data types."""
import sys
import inspect import inspect
from collections import OrderedDict
from secop.errors import ProgrammingError, ConfigError, BadValueError from secop.errors import ConfigError, ProgrammingError, BadValueError
def flatten_dict(dictname, itemcls, attrs, remove=True): class HasDescriptorMeta(type):
properties = {} def __new__(cls, name, bases, attrs):
# allow to declare properties directly as class attribute newtype = type.__new__(cls, name, bases, attrs)
# all these attributes are removed if sys.version_info < (3, 6):
for k, v in attrs.items(): # support older python versions
if isinstance(v, tuple) and v and isinstance(v[0], itemcls): for key, attr in attrs.items():
# this might happen when migrating from old to new style if hasattr(attr, '__set_name__'):
raise ProgrammingError('declared %r with trailing comma' % k) attr.__set_name__(newtype, key)
if isinstance(v, itemcls): newtype.__init_subclass__()
properties[k] = v return newtype
if remove:
for k in properties:
attrs.pop(k) class HasDescriptors(metaclass=HasDescriptorMeta):
properties.update(attrs.get(dictname, {})) @classmethod
attrs[dictname] = properties def __init_subclass__(cls):
# when migrating old style declarations, sometimes the trailing comma is not removed
bad = [k for k, v in cls.__dict__.items()
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
if bad:
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
@classmethod
def filterDescriptors(cls, filter_type):
res = {}
for name in dir(cls):
desc = getattr(cls, name, None)
if isinstance(desc, filter_type):
res[name] = desc
return res
UNSET = object() # an unset value, not even None
# storage for 'properties of a property' # storage for 'properties of a property'
@ -56,7 +73,8 @@ class Property:
:param default: a default value. SECoP properties are normally not sent to the ECS, :param default: a default value. SECoP properties are normally not sent to the ECS,
when they match the default when they match the default
:param extname: external name :param extname: external name
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given :param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
special value 'always': export also when matching the default
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value :param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute) assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
:param settable: settable from the cfg file :param settable: settable from the cfg file
@ -64,142 +82,125 @@ class Property:
# note: this is intended to be used on base classes. # note: this is intended to be used on base classes.
# the VALUES of the properties are on the instances! # the VALUES of the properties are on the instances!
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True): def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
settable=True, value=UNSET, name=''):
if not callable(datatype): if not callable(datatype):
raise ValueError('datatype MUST be a valid DataType or a basic_validator') raise ValueError('datatype MUST be a valid DataType or a basic_validator')
self.description = inspect.cleandoc(description) self.description = inspect.cleandoc(description)
self.default = datatype.default if default is None else datatype(default) self.default = datatype.default if default is UNSET else datatype(default)
self.datatype = datatype self.datatype = datatype
self.extname = extname self.extname = extname
self.export = export or bool(extname) self.export = export or bool(extname)
if mandatory is None: if mandatory is None:
mandatory = default is None mandatory = default is UNSET
self.mandatory = mandatory self.mandatory = mandatory
self.settable = settable or mandatory # settable means settable from the cfg file self.settable = settable or mandatory # settable means settable from the cfg file
self.value = UNSET if value is UNSET else datatype(value)
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.propertyValues.get(self.name, self.default)
def __set__(self, instance, value):
instance.propertyValues[self.name] = self.datatype(value)
def __set_name__(self, owner, name):
self.name = name
if self.export and not self.extname:
self.extname = '_' + name
if self.description == '_':
# the programmer indicates, that the name is already speaking for itself
self.description = name.replace('_', ' ')
def __repr__(self): def __repr__(self):
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % ( extras = ['default=%s' % repr(self.default)]
self.description, self.datatype, self.default, self.extname, self.export, if self.export:
self.mandatory, self.settable) extras.append('extname=%r' % self.extname)
extras.append('export=%r' % self.export)
if self.mandatory:
extras.append('mandatory=True')
if not self.settable:
extras.append('settable=False')
if self.value is not UNSET:
extras.append('value=%s' % repr(self.value))
if not self.name:
extras.append('name=%r' % self.name)
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
class Properties(OrderedDict): class HasProperties(HasDescriptors):
"""a collection of `Property` objects propertyValues = None
checks values upon assignment.
You can either assign a Property object, or a value
(which must pass the validator of the already existing Property)
"""
def __setitem__(self, key, value):
if not isinstance(value, Property):
raise ProgrammingError('setting property %r on classes is not supported!' % key)
# make sure, extname is valid if export is True
if not value.extname and value.export:
value.extname = '_%s' % key # generate custom key
elif value.extname and not value.export:
value.export = True
OrderedDict.__setitem__(self, key, value)
def __delitem__(self, key):
raise ProgrammingError('deleting Properties is not supported!')
class PropertyMeta(type):
"""Metaclass for HasProperties
joining the class's properties with those of base classes.
"""
def __new__(cls, name, bases, attrs):
newtype = type.__new__(cls, name, bases, attrs)
if '__constructed__' in attrs:
return newtype
flatten_dict('properties', Property, attrs)
newtype = cls.__join_properties__(newtype, name, bases, attrs)
attrs['__constructed__'] = True
return newtype
@classmethod
def __join_properties__(cls, newtype, name, bases, attrs):
# merge properties from all sub-classes
properties = Properties()
for base in reversed(bases):
properties.update(getattr(base, "properties", {}))
# update with properties from new class
properties.update(attrs.get('properties', {}))
newtype.properties = properties
# generate getters
for k, po in properties.items():
def getter(self, pname=k):
val = self.__class__.properties[pname].default
return self.properties.get(pname, val)
if k in attrs and not isinstance(attrs[k], (property, Property)):
if callable(attrs[k]):
raise ProgrammingError('%r: property %r collides with method'
% (newtype, k))
# store the attribute value for putting on the instance later
try:
# for inheritance reasons, it seems best to store it as a renamed attribute
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
except BadValueError:
raise ProgrammingError('%r: property %r can not be set to %r'
% (newtype, k, attrs[k]))
setattr(newtype, k, property(getter))
return newtype
class HasProperties(metaclass=PropertyMeta):
properties = {}
def __init__(self): def __init__(self):
super(HasProperties, self).__init__() super(HasProperties, self).__init__()
self.initProperties()
def initProperties(self):
# store property values in the instance, keep descriptors on the class # store property values in the instance, keep descriptors on the class
self.properties = {} self.propertyValues = {}
# pre-init with properties default value (if any) # pre-init
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
value = getattr(self, '_initProp_' + pn, self) if po.value is not UNSET:
if value is not self: # property value was given as attribute self.setProperty(pn, po.value)
self.properties[pn] = value
elif not po.mandatory: @classmethod
self.properties[pn] = po.default def __init_subclass__(cls):
super().__init_subclass__()
# raise an error when an attribute is a tuple with one single descriptor as element
# when migrating old style declarations, sometimes the trailing comma is not removed
bad = [k for k, v in cls.__dict__.items()
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
if bad:
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
properties = {}
for base in cls.__bases__:
properties.update(getattr(base, 'propertyDict', {}))
properties.update(cls.filterDescriptors(Property))
cls.propertyDict = properties
# treat overriding properties with bare values
for pn, po in properties.items():
value = cls.__dict__.get(pn, po)
if not isinstance(value, Property): # attribute is a bare value
po = Property(**po.__dict__)
try:
po.value = po.datatype(value)
except BadValueError:
for base in cls.__bases__:
if pn in getattr(base, 'propertyDict', {}):
if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__))
raise ProgrammingError('can not set property %s.%s to %r' %
(cls.__name__, pn, value))
cls.propertyDict[pn] = po
def checkProperties(self): def checkProperties(self):
"""validates properties and checks for min... <= max...""" """validates properties and checks for min... <= max..."""
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
if po.export and po.mandatory: if po.mandatory:
if pn not in self.properties: if pn not in self.propertyDict:
name = getattr(self, 'name', self.__class__.__name__) name = getattr(self, 'name', self.__class__.__name__)
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype)) raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
# apply validator (which may complain further) # apply validator (which may complain further)
self.properties[pn] = po.datatype(self.properties[pn]) self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
if pn.startswith('min'): if pn.startswith('min'):
maxname = 'max' + pn[3:] maxname = 'max' + pn[3:]
minval = self.properties[pn] minval = self.propertyValues.get(pn, po.default)
maxval = self.properties.get(maxname, minval) maxval = self.propertyValues.get(maxname, minval)
if minval > maxval: if minval > maxval:
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self)) raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
def getProperties(self): def getProperties(self):
return self.__class__.properties return self.propertyDict
def exportProperties(self): def exportProperties(self):
# export properties which have # export properties which have
# export=True and # export=True and
# mandatory=True or non_default=True # mandatory=True or non_default=True
res = {} res = {}
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
val = self.properties.get(pn, None) val = self.propertyValues.get(pn, po.default)
if po.export and (po.mandatory or val != po.default): if po.export and (po.export == 'always' or val != po.default):
try: try:
val = po.datatype.export_value(val) val = po.datatype.export_value(val)
except AttributeError: except AttributeError:
@ -208,4 +209,7 @@ class HasProperties(metaclass=PropertyMeta):
return res return res
def setProperty(self, key, value): def setProperty(self, key, value):
self.properties[key] = self.__class__.properties[key].datatype(value) # this is overwritten by Param.setProperty and DataType.setProperty
# in oder to extend setting to inner properties
# otherwise direct setting of self.<key> = value is preferred
self.propertyValues[key] = self.propertyDict[key].datatype(value)

View File

@ -42,7 +42,7 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from time import time as currenttime from time import time as currenttime
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \ from secop.errors import NoSuchCommandError, NoSuchModuleError, \
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
from secop.params import Parameter from secop.params import Parameter
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
@ -109,7 +109,7 @@ class Dispatcher:
self._subscriptions.setdefault(eventname, set()).add(conn) self._subscriptions.setdefault(eventname, set()).add(conn)
def unsubscribe(self, conn, eventname): def unsubscribe(self, conn, eventname):
if not ':' in eventname: if ':' not in eventname:
# also remove 'more specific' subscriptions # also remove 'more specific' subscriptions
for k, v in self._subscriptions.items(): for k, v in self._subscriptions.items():
if k.startswith('%s:' % eventname): if k.startswith('%s:' % eventname):
@ -177,7 +177,7 @@ class Dispatcher:
result = {'modules': OrderedDict()} result = {'modules': OrderedDict()}
for modulename in self._export: for modulename in self._export:
module = self.get_module(modulename) module = self.get_module(modulename)
if not module.properties.get('export', False): if not module.export:
continue continue
# some of these need rework ! # some of these need rework !
mod_desc = {'accessibles': self.export_accessibles(modulename)} mod_desc = {'accessibles': self.export_accessibles(modulename)}
@ -186,7 +186,7 @@ class Dispatcher:
result['modules'][modulename] = mod_desc result['modules'][modulename] = mod_desc
result['equipment_id'] = self.equipment_id result['equipment_id'] = self.equipment_id
result['firmware'] = 'FRAPPY - The Python Framework for SECoP' result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
result['version'] = '2019.08' result['version'] = '2021.02'
result.update(self.nodeprops) result.update(self.nodeprops)
return result return result
@ -195,40 +195,24 @@ class Dispatcher:
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
cmdname = moduleobj.commands.exported.get(exportedname, None) cname = moduleobj.accessiblename2attr.get(exportedname)
if cmdname is None: cobj = moduleobj.commands.get(cname)
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname)) if cobj is None:
cmdspec = moduleobj.commands[cmdname] raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
if argument is None and cmdspec.datatype.argument is not None:
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
if argument is not None and cmdspec.datatype.argument is None:
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
if cmdspec.datatype.argument:
# validate!
argument = cmdspec.datatype(argument)
# now call func # now call func
# note: exceptions are handled in handle_request, not here! # note: exceptions are handled in handle_request, not here!
func = getattr(moduleobj, 'do_' + cmdname) return cobj.do(moduleobj, argument), dict(t=currenttime())
res = func() if argument is None else func(argument)
# pipe through cmdspec.datatype.result
if cmdspec.datatype.result:
res = cmdspec.datatype.result(res)
return res, dict(t=currenttime())
def _setParameterValue(self, modulename, exportedname, value): def _setParameterValue(self, modulename, exportedname, value):
moduleobj = self.get_module(modulename) moduleobj = self.get_module(modulename)
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
pname = moduleobj.parameters.exported.get(exportedname, None) pname = moduleobj.accessiblename2attr.get(exportedname)
if pname is None: pobj = moduleobj.parameters.get(pname)
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) if pobj is None:
pobj = moduleobj.parameters[pname] raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
if pobj.constant is not None: if pobj.constant is not None:
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely" raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
% (modulename, pname)) % (modulename, pname))
@ -252,10 +236,10 @@ class Dispatcher:
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
pname = moduleobj.parameters.exported.get(exportedname, None) pname = moduleobj.accessiblename2attr.get(exportedname)
if pname is None: pobj = moduleobj.parameters.get(pname)
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) if pobj is None:
pobj = moduleobj.parameters[pname] raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
if pobj.constant is not None: if pobj.constant is not None:
# really needed? we could just construct a readreply instead.... # really needed? we could just construct a readreply instead....
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') # raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
@ -321,8 +305,6 @@ class Dispatcher:
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data))) return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
def handle_do(self, conn, specifier, data): def handle_do(self, conn, specifier, data):
# XXX: should this be done asyncron? we could just return the reply in
# that case
modulename, cmd = specifier.split(':', 1) modulename, cmd = specifier.split(':', 1)
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data))) return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))

View File

@ -28,14 +28,11 @@ from secop.properties import Property
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.lib import get_class from secop.lib import get_class
from secop.client import SecopClient, decode_msg, encode_msg_frame from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError from secop.errors import ConfigError, make_secop_error, CommunicationFailedError, BadValueError
class ProxyModule(HasIodev, Module): class ProxyModule(HasIodev, Module):
properties = { module = Property('remote module name', datatype=StringType(), default='')
'module':
Property('remote module name', datatype=StringType(), default=''),
}
pollerClass = None pollerClass = None
_consistency_check_done = False _consistency_check_done = False
@ -55,7 +52,7 @@ class ProxyModule(HasIodev, Module):
def initModule(self): def initModule(self):
if not self.module: if not self.module:
self.properties['module'] = self.name self.module = self.name
self._secnode = self._iodev.secnode self._secnode = self._iodev.secnode
self._secnode.register_callback(self.module, self.updateEvent, self._secnode.register_callback(self.module, self.updateEvent,
self.descriptiveDataChange, self.nodeStateChange) self.descriptiveDataChange, self.nodeStateChange)
@ -103,9 +100,9 @@ class ProxyModule(HasIodev, Module):
dt = props['datatype'] dt = props['datatype']
try: try:
cobj.datatype.compatible(dt) cobj.datatype.compatible(dt)
except Exception: except BadValueError:
self.log.warning('remote command %s:%s is not compatible: %r != %r' self.log.warning('remote command %s:%s is not compatible: %r != %r'
% (self.module, pname, pobj.datatype, dt)) % (self.module, cname, cobj.datatype, dt))
# what to do if descriptive data does not match? # what to do if descriptive data does not match?
# we might raise an exception, but this would lead to a reconnection, # we might raise an exception, but this would lead to a reconnection,
# which might not help. # which might not help.
@ -141,14 +138,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
class SecNode(Module): class SecNode(Module):
properties = { uri = Property('uri of a SEC node', datatype=StringType())
'uri':
Property('uri of a SEC node', datatype=StringType()),
}
commands = {
'request':
Command('send a request', argument=StringType(), result=StringType())
}
def earlyInit(self): def earlyInit(self):
self.secnode = SecopClient(self.uri, self.log) self.secnode = SecopClient(self.uri, self.log)
@ -156,8 +146,9 @@ class SecNode(Module):
def startModule(self, started_callback): def startModule(self, started_callback):
self.secnode.spawn_connect(started_callback) self.secnode.spawn_connect(started_callback)
def do_request(self, msg): @Command(StringType(), result=StringType())
"""for test purposes""" def request(self, msg):
"""send a request, for debugging purposes"""
reply = self.secnode.request(*decode_msg(msg.encode('utf-8'))) reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
return encode_msg_frame(*reply).decode('utf-8') return encode_msg_frame(*reply).decode('utf-8')
@ -184,17 +175,12 @@ def proxy_class(remote_class, name=None):
else: else:
raise ConfigError('%r is no SECoP module class' % remote_class) raise ConfigError('%r is no SECoP module class' % remote_class)
parameters = {} attrs = rcls.propertyDict.copy()
commands = {}
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
for aname, aobj in rcls.accessibles.items(): for aname, aobj in rcls.accessibles.items():
if isinstance(aobj, Parameter): if isinstance(aobj, Parameter):
pobj = aobj.copy() pobj = aobj.override(poll=False, handler=None, needscfg=False)
parameters[aname] = pobj attrs[aname] = pobj
pobj.properties['poll'] = False
pobj.properties['handler'] = None
pobj.properties['needscfg'] = False
def rfunc(self, pname=aname): def rfunc(self, pname=aname):
value, _, readerror = self._secnode.getParameter(self.name, pname) value, _, readerror = self._secnode.getParameter(self.name, pname)
@ -216,12 +202,11 @@ def proxy_class(remote_class, name=None):
elif isinstance(aobj, Command): elif isinstance(aobj, Command):
cobj = aobj.copy() cobj = aobj.copy()
commands[aname] = cobj
def cfunc(self, arg=None, cname=aname): def cfunc(self, arg=None, cname=aname):
return self._secnode.execCommand(self.name, cname, arg) return self._secnode.execCommand(self.name, cname, arg)
attrs['do_' + aname] = cfunc attrs[aname] = cobj(cfunc)
else: else:
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class)) raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))

View File

@ -227,7 +227,7 @@ class Server:
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
self.log.info('registering module %r' % modname) self.log.info('registering module %r' % modname)
self.dispatcher.register_module(modobj, modname, modobj.properties['export']) self.dispatcher.register_module(modobj, modname, modobj.export)
if modobj.pollerClass is not None: if modobj.pollerClass is not None:
# a module might be explicitly excluded from polling by setting pollerClass to None # a module might be explicitly excluded from polling by setting pollerClass to None
modobj.pollerClass.add_to_table(poll_table, modobj) modobj.pollerClass.add_to_table(poll_table, modobj)
@ -236,10 +236,10 @@ class Server:
# handle attached modules # handle attached modules
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
for propname, propobj in modobj.__class__.properties.items(): for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached): if isinstance(propobj, Attached):
setattr(modobj, propobj.attrname or '_' + propname, setattr(modobj, propobj.attrname or '_' + propname,
self.dispatcher.get_module(modobj.properties[propname])) self.dispatcher.get_module(getattr(modobj, propname)))
# call init on each module after registering all # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
modobj.initModule() modobj.initModule()

View File

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

View File

@ -27,11 +27,10 @@ import time
import threading import threading
import re import re
from secop.lib.asynconn import AsynConn, ConnectionClosed from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Done
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
from secop.poller import REGULAR from secop.poller import REGULAR
from secop.metaclass import Done
class StringIO(Communicator): class StringIO(Communicator):
@ -39,38 +38,22 @@ class StringIO(Communicator):
self healing is assured by polling the parameter 'is_connected' self healing is assured by polling the parameter 'is_connected'
""" """
properties = { uri = Property('hostname:portnumber', datatype=StringType())
'uri': end_of_line = Property('end_of_line character', datatype=ValueType(),
Property('hostname:portnumber', datatype=StringType()), default='\n', settable=True)
'end_of_line': encoding = Property('used encoding', datatype=StringType(),
Property('end_of_line character', datatype=ValueType(), default='ascii', settable=True)
default='\n', settable=True), identification = Property('''
'encoding':
Property('used encoding', datatype=StringType(),
default='ascii', settable=True),
'identification':
Property('''
identification identification
a list of tuples with commands and expected responses as regexp, a list of tuples with commands and expected responses as regexp,
to be sent on connect''', to be sent on connect''',
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False), datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
}
parameters = { timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
'timeout': wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
Parameter('timeout', datatype=FloatRange(0), default=2), is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
'wait_before': pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
Parameter('wait time before sending', datatype=FloatRange(), default=0),
'is_connected':
Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR),
'pollinterval':
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
}
commands = {
'multicomm':
Command('execute multiple commands in one go',
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
}
_reconnectCallbacks = None _reconnectCallbacks = None
@ -105,11 +88,12 @@ class StringIO(Communicator):
self._conn = AsynConn(uri, self._eol_read) self._conn = AsynConn(uri, self._eol_read)
self.is_connected = True self.is_connected = True
for command, regexp in self.identification: for command, regexp in self.identification:
reply = self.do_communicate(command) reply = self.communicate(command)
if not re.match(regexp, reply): if not re.match(regexp, reply):
self.closeConnection() self.closeConnection()
raise CommunicationFailedError('bad response: %s does not match %s' % raise CommunicationFailedError('bad response: %s does not match %s' %
(reply, regexp)) (reply, regexp))
def closeConnection(self): def closeConnection(self):
"""close connection """close connection
@ -170,7 +154,7 @@ class StringIO(Communicator):
if removeme: if removeme:
self._reconnectCallbacks.pop(key) self._reconnectCallbacks.pop(key)
def do_communicate(self, command): def communicate(self, command):
"""send a command and receive a reply """send a command and receive a reply
using end_of_line, encoding and self._lock using end_of_line, encoding and self._lock
@ -179,6 +163,8 @@ class StringIO(Communicator):
""" """
if not self.is_connected: if not self.is_connected:
self.read_is_connected() # try to reconnect self.read_is_connected() # try to reconnect
if not self._conn:
raise CommunicationSilentError('can not connect to %r' % self.uri)
try: try:
with self._lock: with self._lock:
# read garbage and wait before send # read garbage and wait before send
@ -210,11 +196,13 @@ class StringIO(Communicator):
self.log.error(self._last_error) self.log.error(self._last_error)
raise raise
def do_multicomm(self, commands): @Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
def multicomm(self, commands):
"""communicate multiple request/replies in one row"""
replies = [] replies = []
with self._lock: with self._lock:
for cmd in commands: for cmd in commands:
replies.append(self.do_communicate(cmd)) replies.append(self.communicate(cmd))
return replies return replies
@ -223,17 +211,15 @@ class HasIodev(Module):
not only StringIO ! not only StringIO !
""" """
properties = { iodev = Attached()
'iodev': Attached(), uri = Property('uri for automatic creation of the attached communication module',
'uri': Property('uri for automatic creation of the attached communication module', StringType(), default='')
StringType(), default=''),
}
iodevDict = {} iodevDict = {}
def __init__(self, name, logger, opts, srv): def __init__(self, name, logger, opts, srv):
iodev = opts.get('iodev') iodev = opts.get('iodev')
super().__init__(name, logger, opts, srv) Module.__init__(self, name, logger, opts, srv)
if self.uri: if self.uri:
opts = {'uri': self.uri, 'description': 'communication device for %s' % name, opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
'export': False} 'export': False}
@ -243,7 +229,9 @@ class HasIodev(Module):
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv) iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
srv.modules[ioname] = iodev srv.modules[ioname] = iodev
self.iodevDict[self.uri] = ioname self.iodevDict[self.uri] = ioname
self.setProperty('iodev', ioname) self.iodev = ioname
elif not self.iodev:
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
def initModule(self): def initModule(self):
try: try:
@ -254,4 +242,4 @@ class HasIodev(Module):
super().initModule() super().initModule()
def sendRecv(self, command): def sendRecv(self, command):
return self._iodev.do_communicate(command) return self._iodev.communicate(command)

View File

@ -27,18 +27,16 @@ from math import atan
from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
from secop.modules import Drivable, Override, Parameter from secop.modules import Drivable, Parameter, Command
# test custom property (value.test can be changed in config file) # test custom property (value.test can be changed in config file)
from secop.properties import Property from secop.properties import Property
Parameter.properties['test'] = Property('A Property for testing purposes', StringType(), default='', export=True) Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
class CryoBase(Drivable): class CryoBase(Drivable):
properties = { is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True)
'is_cryo': Property('private Flag if this is a cryostat', BoolType(), default=True, export=True),
}
class Cryostat(CryoBase): class Cryostat(CryoBase):
@ -49,7 +47,6 @@ class Cryostat(CryoBase):
- thermal transfer between regulation and samplen - thermal transfer between regulation and samplen
""" """
parameters = dict(
jitter = Parameter("amount of random noise on readout values", jitter = Parameter("amount of random noise on readout values",
datatype=FloatRange(0, 1), unit="K", datatype=FloatRange(0, 1), unit="K",
default=0.1, readonly=False, export=False, default=0.1, readonly=False, export=False,
@ -82,11 +79,11 @@ class Cryostat(CryoBase):
datatype=FloatRange(0), default=0, unit="W", datatype=FloatRange(0), default=0, unit="W",
group='heater_settings', group='heater_settings',
), ),
target=Override("target temperature", target = Parameter("target temperature",
datatype=FloatRange(0), default=0, unit="K", datatype=FloatRange(0), default=0, unit="K",
readonly=False, readonly=False,
), ),
value=Override("regulation temperature", value = Parameter("regulation temperature",
datatype=FloatRange(0), default=0, unit="K", datatype=FloatRange(0), default=0, unit="K",
test='TEST', test='TEST',
), ),
@ -96,6 +93,7 @@ class Cryostat(CryoBase):
default=(40, 10, 2), readonly=False, default=(40, 10, 2), readonly=False,
group='pid', group='pid',
), ),
# pylint: disable=invalid-name
p = Parameter("regulation coefficient 'p'", p = Parameter("regulation coefficient 'p'",
datatype=FloatRange(0), default=40, unit="%/K", readonly=False, datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
group='pid', group='pid',
@ -113,7 +111,7 @@ class Cryostat(CryoBase):
default='ramp', default='ramp',
readonly=False, readonly=False,
), ),
pollinterval=Override("polling interval", pollinterval = Parameter("polling interval",
datatype=FloatRange(0), default=5, datatype=FloatRange(0), default=5,
), ),
tolerance = Parameter("temperature range for stability checking", tolerance = Parameter("temperature range for stability checking",
@ -131,11 +129,6 @@ class Cryostat(CryoBase):
readonly=False, readonly=False,
group='stability', group='stability',
), ),
)
commands = dict(
stop=Override(
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"),
)
def initModule(self): def initModule(self):
self._stopflag = False self._stopflag = False
@ -180,8 +173,11 @@ class Cryostat(CryoBase):
def read_pid(self): def read_pid(self):
return (self.p, self.i, self.d) return (self.p, self.i, self.d)
def do_stop(self): @Command()
# stop the ramp by setting current setpoint as target def stop(self):
"""Stop ramping the setpoint
by setting the current setpoint as new target"""
# XXX: discussion: take setpoint or current value ??? # XXX: discussion: take setpoint or current value ???
self.write_target(self.setpoint) self.write_target(self.setpoint)

View File

@ -28,42 +28,39 @@ import time
from secop.datatypes import ArrayOf, BoolType, EnumType, \ from secop.datatypes import ArrayOf, BoolType, EnumType, \
FloatRange, IntRange, StringType, StructOf, TupleOf FloatRange, IntRange, StringType, StructOf, TupleOf
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Drivable, Override, Parameter as SECoP_Parameter, Readable from secop.modules import Drivable, Parameter as SECoP_Parameter, Readable
from secop.properties import Property from secop.properties import Property
class Parameter(SECoP_Parameter): class Parameter(SECoP_Parameter):
properties = { test = Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test')
'test' : Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test'),
}
PERSIST = 101 PERSIST = 101
class Switch(Drivable): class Switch(Drivable):
"""switch it on or off.... """switch it on or off....
""" """
parameters = {
'value': Override('current state (on or off)', value = Parameter('current state (on or off)',
datatype=EnumType(on=1, off=0), default=0, datatype=EnumType(on=1, off=0), default=0,
), )
'target': Override('wanted state (on or off)', target = Parameter('wanted state (on or off)',
datatype=EnumType(on=1, off=0), default=0, datatype=EnumType(on=1, off=0), default=0,
readonly=False, readonly=False,
), )
'switch_on_time': Parameter('seconds to wait after activating the switch', switch_on_time = Parameter('seconds to wait after activating the switch',
datatype=FloatRange(0, 60), unit='s', datatype=FloatRange(0, 60), unit='s',
default=10, export=False, default=10, export=False,
), )
'switch_off_time': Parameter('cool-down time in seconds', switch_off_time = Parameter('cool-down time in seconds',
datatype=FloatRange(0, 60), unit='s', datatype=FloatRange(0, 60), unit='s',
default=10, export=False, default=10, export=False,
), )
}
properties = { description = Property('The description of the Module', StringType(),
'description' : Property('The description of the Module', StringType(), default='no description', mandatory=False, extname='description')
default='no description', mandatory=False, extname='description'),
}
def read_value(self): def read_value(self):
# could ask HW # could ask HW
@ -109,30 +106,29 @@ class Switch(Drivable):
class MagneticField(Drivable): class MagneticField(Drivable):
"""a liquid magnet """a liquid magnet
""" """
parameters = {
'value': Override('current field in T', value = Parameter('current field in T',
unit='T', datatype=FloatRange(-15, 15), default=0, unit='T', datatype=FloatRange(-15, 15), default=0,
), )
'target': Override('target field in T', target = Parameter('target field in T',
unit='T', datatype=FloatRange(-15, 15), default=0, unit='T', datatype=FloatRange(-15, 15), default=0,
readonly=False, readonly=False,
), )
'ramp': Parameter('ramping speed', ramp = Parameter('ramping speed',
unit='T/min', datatype=FloatRange(0, 1), default=0.1, unit='T/min', datatype=FloatRange(0, 1), default=0.1,
readonly=False, readonly=False,
), )
'mode': Parameter('what to do after changing field', mode = Parameter('what to do after changing field',
default=1, datatype=EnumType(persistent=1, hold=0), default=1, datatype=EnumType(persistent=1, hold=0),
readonly=False, readonly=False,
), )
'heatswitch': Parameter('name of heat switch device', heatswitch = Parameter('name of heat switch device',
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
}
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303) Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
overrides = {
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())), status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
}
def initModule(self): def initModule(self):
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
@ -202,21 +198,20 @@ class MagneticField(Drivable):
time.sleep(max(0.01, ts + loopdelay - time.time())) time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!') self.log.error(self, 'main thread exited unexpectedly!')
def do_stop(self): def stop(self):
self.write_target(self.read_value()) self.write_target(self.read_value())
class CoilTemp(Readable): class CoilTemp(Readable):
"""a coil temperature """a coil temperature
""" """
parameters = {
'value': Override('Coil temperatur', value = Parameter('Coil temperatur',
unit='K', datatype=FloatRange(), default=0, unit='K', datatype=FloatRange(), default=0,
), )
'sensor': Parameter("Sensor number or calibration id", sensor = Parameter("Sensor number or calibration id",
datatype=StringType(), readonly=True, datatype=StringType(), readonly=True,
), )
}
def read_value(self): def read_value(self):
return round(2.3 + random.random(), 3) return round(2.3 + random.random(), 3)
@ -225,18 +220,17 @@ class CoilTemp(Readable):
class SampleTemp(Drivable): class SampleTemp(Drivable):
"""a sample temperature """a sample temperature
""" """
parameters = {
'value': Override('Sample temperature', value = Parameter('Sample temperature',
unit='K', datatype=FloatRange(), default=10, unit='K', datatype=FloatRange(), default=10,
), )
'sensor': Parameter("Sensor number or calibration id", sensor = Parameter("Sensor number or calibration id",
datatype=StringType(), readonly=True, datatype=StringType(), readonly=True,
), )
'ramp': Parameter('moving speed in K/min', ramp = Parameter('moving speed in K/min',
datatype=FloatRange(0, 100), unit='K/min', default=0.1, datatype=FloatRange(0, 100), unit='K/min', default=0.1,
readonly=False, readonly=False,
), )
}
def initModule(self): def initModule(self):
_thread = threading.Thread(target=self._thread) _thread = threading.Thread(target=self._thread)
@ -272,20 +266,19 @@ class Label(Readable):
of several subdevices. used for demoing connections between of several subdevices. used for demoing connections between
modules. modules.
""" """
parameters = {
'system': Parameter("Name of the magnet system", system = Parameter("Name of the magnet system",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'subdev_mf': Parameter("name of subdevice for magnet status", subdev_mf = Parameter("name of subdevice for magnet status",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'subdev_ts': Parameter("name of subdevice for sample temp", subdev_ts = Parameter("name of subdevice for sample temp",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'value': Override("final value of label string", default='', value = Parameter("final value of label string", default='',
datatype=StringType(), datatype=StringType(),
), )
}
def read_value(self): def read_value(self):
strings = [self.system] strings = [self.system]
@ -317,29 +310,25 @@ class Label(Readable):
class DatatypesTest(Readable): class DatatypesTest(Readable):
"""for demoing all datatypes """for demoing all datatypes
""" """
parameters = {
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9), enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
readonly=False, default=1), readonly=False, default=1)
'tupleof': Parameter('tuple of int, float and str', tupleof = Parameter('tuple of int, float and str',
datatype=TupleOf(IntRange(), FloatRange(), datatype=TupleOf(IntRange(), FloatRange(),
StringType()), StringType()),
readonly=False, default=(1, 2.3, 'a')), readonly=False, default=(1, 2.3, 'a'))
'arrayof': Parameter('array: 2..3 times bool', arrayof = Parameter('array: 2..3 times bool',
datatype=ArrayOf(BoolType(), 2, 3), datatype=ArrayOf(BoolType(), 2, 3),
readonly=False, default=[1, 0, 1]), readonly=False, default=[1, 0, 1])
'intrange': Parameter('intrange', datatype=IntRange(2, 9), intrange = Parameter('intrange', datatype=IntRange(2, 9),
readonly=False, default=4), readonly=False, default=4)
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1), floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
readonly=False, default=0, ), readonly=False, default=0)
'struct': Parameter('struct(a=str, b=int, c=bool)', struct = Parameter('struct(a=str, b=int, c=bool)',
datatype=StructOf(a=StringType(), b=IntRange(), datatype=StructOf(a=StringType(), b=IntRange(),
c=BoolType()), c=BoolType()))
),
}
class ArrayTest(Readable): class ArrayTest(Readable):
parameters = { x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000), default=100000 * [0])
default = 100000 * [0]),
}

View File

@ -24,7 +24,7 @@
import random import random
from secop.datatypes import FloatRange, StringType from secop.datatypes import FloatRange, StringType
from secop.modules import Communicator, Drivable, Parameter, Readable, Override from secop.modules import Communicator, Drivable, Parameter, Readable
from secop.params import Command from secop.params import Command
@ -45,11 +45,10 @@ class Heater(Drivable):
class name indicates it to be some heating element, class name indicates it to be some heating element,
but the implementation may do anything but the implementation may do anything
""" """
parameters = {
'maxheaterpower': Parameter('maximum allowed heater power', maxheaterpower = Parameter('maximum allowed heater power',
datatype=FloatRange(0, 100), unit='W', datatype=FloatRange(0, 100), unit='W',
), )
}
def read_value(self): def read_value(self):
return round(100 * random.random(), 1) return round(100 * random.random(), 1)
@ -64,22 +63,21 @@ class Temp(Drivable):
class name indicates it to be some temperature controller, class name indicates it to be some temperature controller,
but the implementation may do anything but the implementation may do anything
""" """
parameters = {
'sensor': Parameter( sensor = Parameter(
"Sensor number or calibration id", "Sensor number or calibration id",
datatype=StringType( datatype=StringType(
8, 8,
16), 16),
readonly=True, readonly=True,
), )
'target': Override( target = Parameter(
"Target temperature", "Target temperature",
default=300.0, default=300.0,
datatype=FloatRange(0), datatype=FloatRange(0),
readonly=False, readonly=False,
unit='K', unit='K',
), )
}
def read_value(self): def read_value(self):
return round(100 * random.random(), 1) return round(100 * random.random(), 1)
@ -90,8 +88,8 @@ class Temp(Drivable):
class Lower(Communicator): class Lower(Communicator):
"""Communicator returning a lowercase version of the request""" """Communicator returning a lowercase version of the request"""
command = {
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'), @Command(argument=StringType(), result=StringType(), export='communicate')
} def communicate(self, command):
def do_communicate(self, request): """lowercase a string"""
return str(request).lower() return str(command).lower()

View File

@ -58,20 +58,20 @@ except ImportError:
class EpicsReadable(Readable): class EpicsReadable(Readable):
"""EpicsDrivable handles a Drivable interfacing to EPICS v4""" """EpicsDrivable handles a Drivable interfacing to EPICS v4"""
# Commmon parameter for all EPICS devices # Commmon parameter for all EPICS devices
parameters = {
'value': Parameter('EPICS generic value', # parameters
value = Parameter('EPICS generic value',
datatype=FloatRange(), datatype=FloatRange(),
default=300.0,), default=300.0,)
'epics_version': Parameter("EPICS version used, v3 or v4", epics_version = Parameter("EPICS version used, v3 or v4",
datatype=EnumType(v3=3, v4=4),), datatype=EnumType(v3=3, v4=4),)
# 'private' parameters: not remotely accessible value_pv = Parameter('EPICS pv_name of value',
'value_pv': Parameter('EPICS pv_name of value',
datatype=StringType(), datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'status_pv': Parameter('EPICS pv_name of status', status_pv = Parameter('EPICS pv_name of status',
datatype=StringType(), datatype=StringType(),
default="unset", export=False), default="unset", export=False)
}
# Generic read and write functions # Generic read and write functions
def _read_pv(self, pv_name): def _read_pv(self, pv_name):
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
class EpicsDrivable(Drivable): class EpicsDrivable(Drivable):
"""EpicsDrivable handles a Drivable interfacing to EPICS v4""" """EpicsDrivable handles a Drivable interfacing to EPICS v4"""
# Commmon parameter for all EPICS devices # Commmon parameter for all EPICS devices
parameters = {
'target': Parameter('EPICS generic target', datatype=FloatRange(), # parameters
default=300.0, readonly=False), target = Parameter('EPICS generic target', datatype=FloatRange(),
'value': Parameter('EPICS generic value', datatype=FloatRange(), default=300.0, readonly=False)
default=300.0,), value = Parameter('EPICS generic value', datatype=FloatRange(),
'epics_version': Parameter("EPICS version used, v3 or v4", default=300.0,)
datatype=StringType(),), epics_version = Parameter("EPICS version used, v3 or v4",
# 'private' parameters: not remotely accessible datatype=StringType(),)
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(), target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(), value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(), status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
}
# Generic read and write functions # Generic read and write functions
def _read_pv(self, pv_name): def _read_pv(self, pv_name):
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
class EpicsTempCtrl(EpicsDrivable): class EpicsTempCtrl(EpicsDrivable):
parameters = {
# TODO: restrict possible values with oneof datatype # parameters
'heaterrange': Parameter('Heater range', datatype=StringType(), heaterrange = Parameter('Heater range', datatype=StringType(),
default='Off', readonly=False,), default='Off', readonly=False,)
'tolerance': Parameter('allowed deviation between value and target', tolerance = Parameter('allowed deviation between value and target',
datatype=FloatRange(1e-6, 1e6), default=0.1, datatype=FloatRange(1e-6, 1e6), default=0.1,
readonly=False,), readonly=False,)
# 'private' parameters: not remotely accessible heaterrange_pv = Parameter('EPICS pv_name of heater range',
'heaterrange_pv': Parameter('EPICS pv_name of heater range', datatype=StringType(), default="unset", export=False,)
datatype=StringType(), default="unset", export=False,),
}
def read_target(self): def read_target(self):
return self._read_pv(self.target_pv) return self._read_pv(self.target_pv)

View File

@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
pollerClass = BasicPoller pollerClass = BasicPoller
parameters = {
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False), # parameters
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False), subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False), subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False), subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
'userlimits': Parameter('User defined limits of device value', subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
userlimits = Parameter('User defined limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10), default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
'abslimits': Parameter('Absolute limits of device value', abslimits = Parameter('Absolute limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
default=(-0.5, 0.5), poll=True, default=(-0.5, 0.5), poll=True,
), )
'precision': Parameter('Precision of the device value (allowed deviation ' precision = Parameter('Precision of the device value (allowed deviation '
'of stable values from target)', 'of stable values from target)',
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False, datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
), )
'ramp': Parameter('Target rate of field change per minute', readonly=False, ramp = Parameter('Target rate of field change per minute', readonly=False,
datatype=FloatRange(unit='$/min'), default=1.0), datatype=FloatRange(unit='$/min'), default=1.0)
'calibration': Parameter('Coefficients for calibration ' calibration = Parameter('Coefficients for calibration '
'function: [c0, c1, c2, c3, c4] calculates ' 'function: [c0, c1, c2, c3, c4] calculates '
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
' in T', poll=1, ' in T', poll=1,
datatype=ArrayOf(FloatRange(), 5, 5), datatype=ArrayOf(FloatRange(), 5, 5),
default=(1.0, 0.0, 0.0, 0.0, 0.0)), default=(1.0, 0.0, 0.0, 0.0, 0.0))
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting', calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5), datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
short=ArrayOf( short=ArrayOf(
FloatRange(), 5, 5), FloatRange(), 5, 5),
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False), asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
}
def _current2field(self, current, *coefficients): def _current2field(self, current, *coefficients):
"""Return field in T for given current in A. """Return field in T for given current in A.
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
return self._currentsource.read_status()[0] == 'BUSY' return self._currentsource.read_status()[0] == 'BUSY'
if self._currentsource.status[0] != 'BUSY': if self._currentsource.status[0] != 'BUSY':
if self._enable.status[0] == 'ERROR': if self._enable.status[0] == 'ERROR':
self._enable.do_reset() self._enable.reset()
self._enable.read_status() self._enable.read_status()
self._enable.write_target('On') self._enable.write_target('On')
self._enable._hw_wait() self._enable._hw_wait()

View File

@ -41,7 +41,7 @@ from secop.errors import CommunicationFailedError, \
ConfigError, HardwareError, ProgrammingError ConfigError, HardwareError, ProgrammingError
from secop.lib import lazy_property from secop.lib import lazy_property
from secop.modules import Command, Drivable, \ from secop.modules import Command, Drivable, \
Module, Override, Parameter, Readable, BasicPoller Module, Parameter, Readable, BasicPoller
##### #####
@ -160,24 +160,18 @@ class PyTangoDevice(Module):
pollerClass = BasicPoller pollerClass = BasicPoller
parameters = { # parameters
'comtries': Parameter('Maximum retries for communication', comtries = Parameter('Maximum retries for communication',
datatype=IntRange(1, 100), default=3, readonly=False, datatype=IntRange(1, 100), default=3, readonly=False,
group='communication'), group='communication')
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0), comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
unit='s', default=0.1, readonly=False, unit='s', default=0.1, readonly=False,
group='communication'), group='communication')
tangodevice = Parameter('Tango device name',
'tangodevice': Parameter('Tango device name',
datatype=StringType(), readonly=True, datatype=StringType(), readonly=True,
# export=True, # for testing only # export=True, # for testing only
export=False, export=False,
), )
}
commands = {
'reset': Command('Tango reset command', argument=None, result=None),
}
tango_status_mapping = { tango_status_mapping = {
PyTango.DevState.ON: Drivable.Status.IDLE, PyTango.DevState.ON: Drivable.Status.IDLE,
@ -372,7 +366,9 @@ class PyTangoDevice(Module):
return (myState, tangoStatus) return (myState, tangoStatus)
def do_reset(self): @Command(argument=None, result=None)
def reset(self):
"""Tango reset command"""
self._dev.Reset() self._dev.Reset()
@ -405,13 +401,9 @@ class Sensor(AnalogInput):
# note: we don't transport the formula to secop.... # note: we don't transport the formula to secop....
# we support the adjust method # we support the adjust method
commands = { @Command(argument=FloatRange(), result=None)
'setposition': Command('Set the position to the given value.', def setposition(self, value):
argument=FloatRange(), result=None, """Set the position to the given value."""
),
}
def do_setposition(self, value):
self._dev.Adjust(value) self._dev.Adjust(value)
@ -427,29 +419,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
controllers, ... controllers, ...
""" """
parameters = { # parameters
'userlimits': Parameter('User defined limits of device value', userlimits = Parameter('User defined limits of device value',
datatype=LimitsType(FloatRange(unit='$')), datatype=LimitsType(FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')), default=(float('-Inf'), float('+Inf')),
readonly=False, poll=10, readonly=False, poll=10,
), )
'abslimits': Parameter('Absolute limits of device value', abslimits = Parameter('Absolute limits of device value',
datatype=LimitsType(FloatRange(unit='$')), datatype=LimitsType(FloatRange(unit='$')),
), )
'precision': Parameter('Precision of the device value (allowed deviation ' precision = Parameter('Precision of the device value (allowed deviation '
'of stable values from target)', 'of stable values from target)',
datatype=FloatRange(1e-38, unit='$'), datatype=FloatRange(1e-38, unit='$'),
readonly=False, group='stability', readonly=False, group='stability',
), )
'window': Parameter('Time window for checking stabilization if > 0', window = Parameter('Time window for checking stabilization if > 0',
default=60.0, readonly=False, default=60.0, readonly=False,
datatype=FloatRange(0, 900, unit='s'), group='stability', datatype=FloatRange(0, 900, unit='s'), group='stability',
), )
'timeout': Parameter('Timeout for waiting for a stable value (if > 0)', timeout = Parameter('Timeout for waiting for a stable value (if > 0)',
default=60.0, readonly=False, default=60.0, readonly=False,
datatype=FloatRange(0, 900, unit='s'), group='stability', datatype=FloatRange(0, 900, unit='s'), group='stability',
), )
}
_history = () _history = ()
_timeout = None _timeout = None
_moving = False _moving = False
@ -566,7 +558,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
if self.status[0] == self.Status.BUSY: if self.status[0] == self.Status.BUSY:
# changing target value during movement is not allowed by the # changing target value during movement is not allowed by the
# Tango base class state machine. If we are moving, stop first. # Tango base class state machine. If we are moving, stop first.
self.do_stop() self.stop()
self._hw_wait() self._hw_wait()
self._dev.value = value self._dev.value = value
# set meaningful timeout # set meaningful timeout
@ -587,7 +579,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY: while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
sleep(0.3) sleep(0.3)
def do_stop(self): def stop(self):
self._dev.Stop() self._dev.Stop()
@ -601,21 +593,14 @@ class Actuator(AnalogOutput):
""" """
# for secop: support the speed and ramp parameters # for secop: support the speed and ramp parameters
parameters = { # parameters
'speed': Parameter('The speed of changing the value', speed = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/s'), readonly=False, datatype=FloatRange(0, unit='$/s'),
), )
'ramp': Parameter('The speed of changing the value', ramp = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/s'), readonly=False, datatype=FloatRange(0, unit='$/s'),
poll=30, poll=30,
), )
}
commands = {
'setposition': Command('Set the position to the given value.',
argument=FloatRange(), result=None,
),
}
def read_speed(self): def read_speed(self):
return self._dev.speed return self._dev.speed
@ -630,7 +615,9 @@ class Actuator(AnalogOutput):
self.write_speed(value / 60.) self.write_speed(value / 60.)
return self.read_speed() * 60 return self.read_speed() * 60
def do_setposition(self, value=FloatRange()): @Command(FloatRange(), result=None)
def setposition(self, value=FloatRange()):
"""Set the position to the given value."""
self._dev.Adjust(value) self._dev.Adjust(value)
@ -641,21 +628,16 @@ class Motor(Actuator):
It has the ability to move a real object from one place to another place. It has the ability to move a real object from one place to another place.
""" """
parameters = { # parameters
'refpos': Parameter('Reference position', refpos = Parameter('Reference position',
datatype=FloatRange(unit='$'), datatype=FloatRange(unit='$'),
), )
'accel': Parameter('Acceleration', accel = Parameter('Acceleration',
datatype=FloatRange(unit='$/s^2'), readonly=False, datatype=FloatRange(unit='$/s^2'), readonly=False,
), )
'decel': Parameter('Deceleration', decel = Parameter('Deceleration',
datatype=FloatRange(unit='$/s^2'), readonly=False, datatype=FloatRange(unit='$/s^2'), readonly=False,
), )
}
commands = {
'reference': Command('Do a reference run', argument=None, result=None),
}
def read_refpos(self): def read_refpos(self):
return float(self._getProperty('refpos')) return float(self._getProperty('refpos'))
@ -672,7 +654,9 @@ class Motor(Actuator):
def write_decel(self, value): def write_decel(self, value):
self._dev.decel = value self._dev.decel = value
def do_reference(self): @Command()
def reference(self):
"""Do a reference run"""
self._dev.Reference() self._dev.Reference()
return self.read_value() return self.read_value()
@ -681,32 +665,29 @@ class TemperatureController(Actuator):
"""A temperature control loop device. """A temperature control loop device.
""" """
parameters = { # parameters
'p': Parameter('Proportional control Parameter', datatype=FloatRange(), # pylint: disable=invalid-name
p = Parameter('Proportional control Parameter', datatype=FloatRange(),
readonly=False, group='pid', readonly=False, group='pid',
), )
'i': Parameter('Integral control Parameter', datatype=FloatRange(), i = Parameter('Integral control Parameter', datatype=FloatRange(),
readonly=False, group='pid', readonly=False, group='pid',
), )
'd': Parameter('Derivative control Parameter', datatype=FloatRange(), d = Parameter('Derivative control Parameter', datatype=FloatRange(),
readonly=False, group='pid', readonly=False, group='pid',
), )
'pid': Parameter('pid control Parameters', pid = Parameter('pid control Parameters',
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
readonly=False, group='pid', poll=30, readonly=False, group='pid', poll=30,
), )
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1, setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
), )
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1, heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
), )
}
overrides = { # overrides
# We want this to be freely user-settable, and not produce a warning precision = Parameter(default=0.1)
# on startup, so select a usually sensible default. ramp = Parameter(description='Temperature ramp')
'precision': Override(default=0.1),
'ramp': Override(description='Temperature ramp'),
}
def read_ramp(self): def read_ramp(self):
return self._dev.ramp return self._dev.ramp
@ -755,15 +736,14 @@ class PowerSupply(Actuator):
"""A power supply (voltage and current) device. """A power supply (voltage and current) device.
""" """
parameters = { # parameters
'voltage': Parameter('Actual voltage', voltage = Parameter('Actual voltage',
datatype=FloatRange(unit='V'), poll=-5), datatype=FloatRange(unit='V'), poll=-5)
'current': Parameter('Actual current', current = Parameter('Actual current',
datatype=FloatRange(unit='A'), poll=-5), datatype=FloatRange(unit='A'), poll=-5)
}
overrides = { # overrides
'ramp': Override(description='Current/voltage ramp'), ramp = Parameter(description='Current/voltage ramp')
}
def read_ramp(self): def read_ramp(self):
return self._dev.ramp return self._dev.ramp
@ -782,9 +762,8 @@ class DigitalInput(PyTangoDevice, Readable):
"""A device reading a bitfield. """A device reading a bitfield.
""" """
overrides = { # overrides
'value': Override(datatype=IntRange()), value = Parameter(datatype=IntRange())
}
def read_value(self): def read_value(self):
return self._dev.value return self._dev.value
@ -794,10 +773,9 @@ class NamedDigitalInput(DigitalInput):
"""A DigitalInput with numeric values mapped to names. """A DigitalInput with numeric values mapped to names.
""" """
parameters = { # parameters
'mapping': Parameter('A dictionary mapping state names to integers', mapping = Parameter('A dictionary mapping state names to integers',
datatype=StringType(), export=False), # XXX:!!! datatype=StringType(), export=False) # XXX:!!!
}
def initModule(self): def initModule(self):
super(NamedDigitalInput, self).initModule() super(NamedDigitalInput, self).initModule()
@ -821,12 +799,11 @@ class PartialDigitalInput(NamedDigitalInput):
bit width accessed. bit width accessed.
""" """
parameters = { # parameters
'startbit': Parameter('Number of the first bit', startbit = Parameter('Number of the first bit',
datatype=IntRange(0), default=0), datatype=IntRange(0), default=0)
'bitwidth': Parameter('Number of bits', bitwidth = Parameter('Number of bits',
datatype=IntRange(0), default=1), datatype=IntRange(0), default=1)
}
def initModule(self): def initModule(self):
super(PartialDigitalInput, self).initModule() super(PartialDigitalInput, self).initModule()
@ -844,10 +821,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
bitfield. bitfield.
""" """
overrides = { # overrides
'value': Override(datatype=IntRange()), value = Parameter(datatype=IntRange())
'target': Override(datatype=IntRange()), target = Parameter(datatype=IntRange())
}
def read_value(self): def read_value(self):
return self._dev.value # mapping is done by datatype upon export() return self._dev.value # mapping is done by datatype upon export()
@ -865,10 +841,9 @@ class NamedDigitalOutput(DigitalOutput):
"""A DigitalOutput with numeric values mapped to names. """A DigitalOutput with numeric values mapped to names.
""" """
parameters = { # parameters
'mapping': Parameter('A dictionary mapping state names to integers', mapping = Parameter('A dictionary mapping state names to integers',
datatype=StringType(), export=False), datatype=StringType(), export=False)
}
def initModule(self): def initModule(self):
super(NamedDigitalOutput, self).initModule() super(NamedDigitalOutput, self).initModule()
@ -894,12 +869,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
bit width accessed. bit width accessed.
""" """
parameters = { # parameters
'startbit': Parameter('Number of the first bit', startbit = Parameter('Number of the first bit',
datatype=IntRange(0), default=0), datatype=IntRange(0), default=0)
'bitwidth': Parameter('Number of bits', bitwidth = Parameter('Number of bits',
datatype=IntRange(0), default=1), datatype=IntRange(0), default=1)
}
def initModule(self): def initModule(self):
super(PartialDigitalOutput, self).initModule() super(PartialDigitalOutput, self).initModule()
@ -925,17 +899,16 @@ class StringIO(PyTangoDevice, Module):
receives strings. receives strings.
""" """
parameters = { # parameters
'bustimeout': Parameter('Communication timeout', bustimeout = Parameter('Communication timeout',
datatype=FloatRange(unit='s'), readonly=False, datatype=FloatRange(unit='s'), readonly=False,
group='communication'), group='communication')
'endofline': Parameter('End of line', endofline = Parameter('End of line',
datatype=StringType(), readonly=False, datatype=StringType(), readonly=False,
group='communication'), group='communication')
'startofline': Parameter('Start of line', startofline = Parameter('Start of line',
datatype=StringType(), readonly=False, datatype=StringType(), readonly=False,
group='communication'), group='communication')
}
def read_bustimeout(self): def read_bustimeout(self):
return self._dev.communicationTimeout return self._dev.communicationTimeout
@ -955,53 +928,48 @@ class StringIO(PyTangoDevice, Module):
def write_startofline(self, value): def write_startofline(self, value):
self._dev.startOfLine = value self._dev.startOfLine = value
commands = { @Command(argument=StringType(), result=StringType())
'communicate': Command('Send a string and return the reply', def communicate(self, value=StringType()):
argument=StringType(), """Send a string and return the reply"""
result=StringType()),
'flush': Command('Flush output buffer',
argument=None, result=None),
'read': Command('read some characters from input buffer',
argument=IntRange(0), result=StringType()),
'write': Command('write some chars to output',
argument=StringType(), result=None),
'readLine': Command('Read sol - a whole line - eol',
argument=None, result=StringType()),
'writeLine': Command('write sol + a whole line + eol',
argument=StringType(), result=None),
'availableChars': Command('return number of chars in input buffer',
argument=None, result=IntRange(0)),
'availableLines': Command('return number of lines in input buffer',
argument=None, result=IntRange(0)),
'multiCommunicate': Command('perform a sequence of communications',
argument=ArrayOf(
TupleOf(StringType(), IntRange()), 100),
result=ArrayOf(StringType(), 100)),
}
def do_communicate(self, value=StringType()):
return self._dev.Communicate(value) return self._dev.Communicate(value)
def do_flush(self): @Command(argument=None, result=None)
def flush(self):
"""Flush output buffer"""
self._dev.Flush() self._dev.Flush()
def do_read(self, value): @Command(argument=IntRange(0), result=StringType())
def read(self, value):
"""read some characters from input buffer"""
return self._dev.Read(value) return self._dev.Read(value)
def do_write(self, value): @Command(argument=StringType(), result=None)
def write(self, value):
"""write some chars to output"""
return self._dev.Write(value) return self._dev.Write(value)
def do_readLine(self): @Command(argument=None, result=StringType())
def readLine(self):
"""Read sol - a whole line - eol"""
return self._dev.ReadLine() return self._dev.ReadLine()
def do_writeLine(self, value): @Command(argument=StringType(), result=None)
def writeLine(self, value):
"""write sol + a whole line + eol"""
return self._dev.WriteLine(value) return self._dev.WriteLine(value)
def do_multiCommunicate(self, value): @Command(argument=ArrayOf(TupleOf(StringType(), IntRange()), 100),
result=ArrayOf(StringType(), 100))
def multiCommunicate(self, value):
"""perform a sequence of communications"""
return self._dev.MultiCommunicate(value) return self._dev.MultiCommunicate(value)
def do_availableChars(self): @Command(argument=None, result=IntRange(0))
def availableChars(self):
"""return number of chars in input buffer"""
return self._dev.availableChars return self._dev.availableChars
def do_availableLines(self): @Command(argument=None, result=IntRange(0))
def availableLines(self):
"""return number of lines in input buffer"""
return self._dev.availableLines return self._dev.availableLines

View File

@ -20,7 +20,7 @@
# ***************************************************************************** # *****************************************************************************
"""Andeen Hagerling capacitance bridge""" """Andeen Hagerling capacitance bridge"""
from secop.core import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done from secop.core import Readable, Parameter, FloatRange, HasIodev, StringIO, Done
class Ah2700IO(StringIO): class Ah2700IO(StringIO):
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
class Capacitance(HasIodev, Readable): class Capacitance(HasIodev, Readable):
parameters = {
'value': Override('capacitance', FloatRange(unit='pF'), poll=True), value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0), freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0), voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
'loss': Parameter('loss', FloatRange(unit='deg'), default=0), loss = Parameter('loss', FloatRange(unit='deg'), default=0)
}
iodevClass = Ah2700IO iodevClass = Ah2700IO
def parse_reply(self, reply): def parse_reply(self, reply):

View File

@ -22,7 +22,7 @@
not tested yet""" not tested yet"""
from secop.core import Writable, Module, Parameter, Override, Attached,\ from secop.core import Writable, Module, Parameter, Attached,\
BoolType, FloatRange, EnumType, HasIodev, StringIO BoolType, FloatRange, EnumType, HasIodev, StringIO
@ -42,13 +42,13 @@ SOURCECMDS = {
class SourceMeter(HasIodev, Module): class SourceMeter(HasIodev, Module):
parameters = {
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True), resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
'power': Parameter('readback power', FloatRange(unit='W'), poll=True), power = Parameter('readback power', FloatRange(unit='W'), poll=True)
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
readonly=False, default=0), readonly=False, default=0)
'active': Parameter('output enable', BoolType(), readonly=False, poll=True), active = Parameter('output enable', BoolType(), readonly=False, poll=True)
}
iodevClass = K2601bIO iodevClass = K2601bIO
def read_resistivity(self): def read_resistivity(self):
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
class Current(HasIodev, Writable): class Current(HasIodev, Writable):
properties = { sourcemeter = Attached()
'sourcemeter': Attached(),
} value = Parameter('measured current', FloatRange(unit='A'), poll=True)
parameters = { target = Parameter('set current', FloatRange(unit='A'), poll=True)
'value': Override('measured current', FloatRange(unit='A'), poll=True), active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage
'target': Override('set current', FloatRange(unit='A'), poll=True), limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True),
}
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.i())') return self.sendRecv('print(smua.measure.i())')
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
class Voltage(HasIodev, Writable): class Voltage(HasIodev, Writable):
properties = { sourcemeter = Attached()
'sourcemeter': Attached(),
} value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
parameters = { target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
'value': Override('measured voltage', FloatRange(unit='V'), poll=True), active = Parameter('voltage is controlled', BoolType(), poll=True)
'target': Override('set voltage', FloatRange(unit='V'), poll=True), limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
'active': Parameter('voltage is controlled', BoolType(), poll=True),
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
}
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.v())') return self.sendRecv('print(smua.measure.v())')

View File

@ -22,8 +22,7 @@
import time import time
from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached from secop.modules import Readable, Drivable, Parameter, Property, Attached, Done
from secop.metaclass import Done
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.poller import Poller, REGULAR from secop.poller import Poller, REGULAR
@ -59,13 +58,11 @@ class StringIO(secop.stringio.StringIO):
class Main(HasIodev, Drivable): class Main(HasIodev, Drivable):
parameters = {
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)), value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
'target': Override('channel to select', datatype=IntRange(0, 17)), target = Parameter('channel to select', datatype=IntRange(0, 17))
'autoscan': autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False), pollinterval = Parameter('sleeptime between polls', default=1)
'pollinterval': Override('sleeptime between polls', default=1),
}
pollerClass = Poller pollerClass = Poller
iodevClass = StringIO iodevClass = StringIO
@ -142,40 +139,23 @@ class ResChannel(HasIodev, Readable):
_main = None # main module _main = None # main module
_last_range_change = 0 # time of last range change _last_range_change = 0 # time of last range change
properties = { channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
'channel': main = Attached()
Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False),
'main':
Attached()
}
parameters = { value = Parameter(datatype=FloatRange(unit='Ohm'))
'value': pollinterval = Parameter(visibility=3)
Override(datatype=FloatRange(unit='Ohm')), range = Parameter('reading range', readonly=False,
'pollinterval': datatype=EnumType(**RES_RANGE), handler=rdgrng)
Override(visibility=3), minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
'range': datatype=EnumType(**RES_RANGE))
Parameter('reading range', readonly=False, autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
datatype=EnumType(**RES_RANGE), handler=rdgrng), readonly=False, handler=rdgrng, default=2)
'minrange': iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng)
Parameter('minimum range for software autorange', readonly=False, default=1, vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng)
datatype=EnumType(**RES_RANGE)), enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset)
'autorange': pause = Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset)
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2), dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset)
readonly=False, handler=rdgrng, default=2), filter = Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl)
'iexc':
Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng),
'vexc':
Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng),
'enabled':
Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset),
'pause':
Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset),
'dwell':
Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset),
'filter':
Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl),
}
def initModule(self): def initModule(self):
self._main = self.DISPATCHER.get_module(self.main) self._main = self.DISPATCHER.get_module(self.main)

View File

@ -41,7 +41,7 @@ class Ls370Sim(Communicator):
self._data[fmt % chan] = v self._data[fmt % chan] = v
# mkthread(self.run) # mkthread(self.run)
def do_communicate(self, command): def communicate(self, command):
# simulation part, time independent # simulation part, time independent
for channel in range(1,17): for channel in range(1,17):
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')

View File

@ -34,8 +34,8 @@ Polling of value and status is done commonly for all modules. For each registere
import time import time
import threading import threading
from secop.modules import Module, Readable, Drivable, Parameter, Override,\ from secop.modules import Readable, Drivable, Parameter,\
Communicator, Property, Attached Communicator, Property, Attached, HasAccessibles, Done
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\ from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
BoolType, StatusType BoolType, StatusType
from secop.lib.enum import Enum from secop.lib.enum import Enum
@ -44,7 +44,6 @@ from secop.errors import HardwareError
from secop.poller import Poller from secop.poller import Poller
import secop.iohandler import secop.iohandler
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.metaclass import Done
try: try:
import secop_psi.ppmswindows as ppmshw import secop_psi.ppmswindows as ppmshw
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
class Main(Communicator): class Main(Communicator):
"""ppms communicator module""" """ppms communicator module"""
parameters = { pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
'pollinterval': Parameter('poll interval', readonly=False, data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
datatype=FloatRange(), default=2), default="", readonly=True)
'communicate': Override('GBIP command'),
'data': Parameter('internal', poll=True, export=True, # export for test only
default="", readonly=True, datatype=StringType()),
}
properties = {
'class_id': Property('Quantum Design class id', export=False,
datatype=StringType()),
}
_channel_names = ['packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2', class_id = Property('Quantum Design class id', StringType(), export=False)
_channel_names = [
'packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2', 'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29'] 'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
assert len(_channel_names) == 30 assert len(_channel_names) == 30
@ -102,7 +96,8 @@ class Main(Communicator):
def register(self, other): def register(self, other):
self.modules[other.channel] = other self.modules[other.channel] = other
def do_communicate(self, command): def communicate(self, command):
"""GPIB command"""
with self.lock: with self.lock:
reply = self._ppms_device.send(command) reply = self._ppms_device.send(command)
self.log.debug("%s|%s", command, reply) self.log.debug("%s|%s", command, reply)
@ -114,7 +109,7 @@ class Main(Communicator):
if channel.enabled: if channel.enabled:
mask |= 1 << self._channel_to_index.get(channelname, 0) mask |= 1 << self._channel_to_index.get(channelname, 0)
# send, read and convert to floats and ints # send, read and convert to floats and ints
data = self.do_communicate('GETDAT? %d' % mask) data = self.communicate('GETDAT? %d' % mask)
reply = data.split(',') reply = data.split(',')
mask = int(reply.pop(0)) mask = int(reply.pop(0))
reply.pop(0) # pop timestamp reply.pop(0) # pop timestamp
@ -133,11 +128,9 @@ class Main(Communicator):
return data # return data as string return data # return data as string
class PpmsMixin(HasIodev, Module): class PpmsMixin(HasIodev, HasAccessibles):
"""common methods for ppms modules""" """common methods for ppms modules"""
properties = { iodev = Attached()
'iodev': Attached(),
}
pollerClass = Poller pollerClass = Poller
enabled = True # default, if no parameter enable is defined enabled = True # default, if no parameter enable is defined
@ -177,28 +170,21 @@ class PpmsMixin(HasIodev, Module):
class Channel(PpmsMixin, Readable): class Channel(PpmsMixin, Readable):
"""channel base class""" """channel base class"""
parameters = {
'value': value = Parameter('main value of channels', poll=True)
Override('main value of channels', poll=True), enabled = Parameter('is this channel used?', readonly=False, poll=False,
'enabled': datatype=BoolType(), default=False)
Parameter('is this channel used?', readonly=False, poll=False, pollinterval = Parameter(visibility=3)
datatype=BoolType(), default=False),
'pollinterval': channel = Property('channel name',
Override(visibility=3), datatype=StringType(), export=False, default='')
} no = Property('channel number',
properties = { datatype=IntRange(1, 4), export=False)
'channel':
Property('channel name',
datatype=StringType(), export=False, default=''),
'no':
Property('channel number',
datatype=IntRange(1, 4), export=False),
}
def earlyInit(self): def earlyInit(self):
Readable.earlyInit(self) Readable.earlyInit(self)
if not self.channel: if not self.channel:
self.properties['channel'] = self.name self.channel = self.name
def get_settings(self, pname): def get_settings(self, pname):
return '' return ''
@ -207,19 +193,12 @@ class Channel(PpmsMixin, Readable):
class UserChannel(Channel): class UserChannel(Channel):
"""user channel""" """user channel"""
parameters = { pollinterval = Parameter(visibility=3)
'pollinterval':
Override(visibility=3),
}
properties = {
'no':
Property('channel number',
datatype=IntRange(0, 0), export=False, default=0),
'linkenable':
Property('name of linked channel for enabling',
datatype=StringType(), export=False, default=''),
} no = Property('channel number',
datatype=IntRange(0, 0), export=False, default=0)
linkenable = Property('name of linked channel for enabling',
datatype=StringType(), export=False, default='')
def write_enabled(self, enabled): def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None) other = self._iodev.modules.get(self.linkenable, None)
@ -233,16 +212,11 @@ class DriverChannel(Channel):
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
parameters = { current = Parameter('driver current', readonly=False, handler=drvout,
'current': datatype=FloatRange(0., 5000., unit='uA'))
Parameter('driver current', readonly=False, handler=drvout, powerlimit = Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 5000., unit='uA')), datatype=FloatRange(0., 1000., unit='uW'))
'powerlimit': pollinterval = Parameter(visibility=3)
Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 1000., unit='uW')),
'pollinterval':
Override(visibility=3),
}
def analyze_drvout(self, no, current, powerlimit): def analyze_drvout(self, no, current, powerlimit):
if self.no != no: if self.no != no:
@ -260,27 +234,19 @@ class BridgeChannel(Channel):
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
# pylint: disable=invalid-name # pylint: disable=invalid-name
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
parameters = {
'enabled': enabled = Parameter(handler=bridge)
Override(handler=bridge), excitation = Parameter('excitation current', readonly=False, handler=bridge,
'excitation': datatype=FloatRange(0.01, 5000., unit='uA'))
Parameter('excitation current', readonly=False, handler=bridge, powerlimit = Parameter('power limit', readonly=False, handler=bridge,
datatype=FloatRange(0.01, 5000., unit='uA')), datatype=FloatRange(0.001, 1000., unit='uW'))
'powerlimit': dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
Parameter('power limit', readonly=False, handler=bridge, datatype=BoolType())
datatype=FloatRange(0.001, 1000., unit='uW')), readingmode = Parameter('reading mode', readonly=False, handler=bridge,
'dcflag': datatype=EnumType(ReadingMode))
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
datatype=BoolType()), datatype=FloatRange(0.0001, 100., unit='mV'))
'readingmode': pollinterval = Parameter(visibility=3)
Parameter('reading mode', readonly=False, handler=bridge,
datatype=EnumType(ReadingMode)),
'voltagelimit':
Parameter('voltage limit', readonly=False, handler=bridge,
datatype=FloatRange(0.0001, 100., unit='mV')),
'pollinterval':
Override(visibility=3),
}
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
if self.no != no: if self.no != no:
@ -306,12 +272,9 @@ class Level(PpmsMixin, Readable):
level = IOHandler('level', 'LEVEL?', '%g,%d') level = IOHandler('level', 'LEVEL?', '%g,%d')
parameters = { value = Parameter(datatype=FloatRange(unit='%'), handler=level)
'value': Override(datatype=FloatRange(unit='%'), handler=level), status = Parameter(handler=level)
'status': Override(handler=level), pollinterval = Parameter(visibility=3)
'pollinterval':
Override(visibility=3),
}
channel = 'level' channel = 'level'
@ -360,16 +323,13 @@ class Chamber(PpmsMixin, Drivable):
venting_continuously=9, venting_continuously=9,
general_failure=15, general_failure=15,
) )
parameters = {
'value': value = Parameter(description='chamber state', handler=chamber,
Override(description='chamber state', handler=chamber, datatype=EnumType(StatusCode))
datatype=EnumType(StatusCode)), target = Parameter(description='chamber command', handler=chamber,
'target': datatype=EnumType(Operation))
Override(description='chamber command', handler=chamber, pollinterval = Parameter(visibility=3)
datatype=EnumType(Operation)),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = { STATUS_MAP = {
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'), StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'), StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
@ -409,37 +369,29 @@ class Temp(PpmsMixin, Drivable):
"""temperature""" """temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
Status = Enum(Drivable.Status, Status = Enum(
Drivable.Status,
RAMPING=370, RAMPING=370,
STABILIZING=380, STABILIZING=380,
) )
# pylint: disable=invalid-name # pylint: disable=invalid-name
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
parameters = {
'value': value = Parameter(datatype=FloatRange(unit='K'), poll=True)
Override(datatype=FloatRange(unit='K'), poll=True), status = Parameter(datatype=StatusType(Status), poll=True)
'status': target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
Override(datatype=StatusType(Status), poll=True), setpoint = Parameter('intermediate set point',
'target': datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False), ramp = Parameter('ramping speed', readonly=False, default=0,
'setpoint': datatype=FloatRange(0, 20, unit='K/min'))
Parameter('intermediate set point', workingramp = Parameter('intermediate ramp value',
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
'ramp': approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
Parameter('ramping speed', readonly=False, default=0, datatype=EnumType(ApproachMode))
datatype=FloatRange(0, 20, unit='K/min')), pollinterval = Parameter(visibility=3)
'workingramp': timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
Parameter('intermediate ramp value', datatype=FloatRange(0, unit='sec'), default=3600)
datatype=FloatRange(0, 20, unit='K/min'), handler=temp),
'approachmode':
Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode)),
'pollinterval':
Override(visibility=3),
'timeout':
Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600),
}
# pylint: disable=invalid-name # pylint: disable=invalid-name
TempStatus = Enum( TempStatus = Enum(
'TempStatus', 'TempStatus',
@ -464,17 +416,14 @@ class Temp(PpmsMixin, Drivable):
14: (Status.ERROR, 'can not complete'), 14: (Status.ERROR, 'can not complete'),
15: (Status.ERROR, 'general failure'), 15: (Status.ERROR, 'general failure'),
} }
properties = { general_stop = Property('respect general stop', datatype=BoolType(),
'general_stop': Property('respect general stop', datatype=BoolType(), default=True, value=False)
export=True, default=True)
}
channel = 'temp' channel = 'temp'
_stopped = False _stopped = False
_expected_target_time = 0 _expected_target_time = 0
_last_change = 0 # 0 means no target change is pending _last_change = 0 # 0 means no target change is pending
_last_target = None # last reached target _last_target = None # last reached target
general_stop = False
_cool_deadline = 0 _cool_deadline = 0
_wait_at10 = False _wait_at10 = False
_ramp_at_limit = False _ramp_at_limit = False
@ -588,7 +537,7 @@ class Temp(PpmsMixin, Drivable):
def calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
if self.status[0] != self.Status.STABILIZING: if self.status[0] != self.Status.STABILIZING:
@ -605,7 +554,8 @@ class Field(PpmsMixin, Drivable):
"""magnetic field""" """magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
Status = Enum(Drivable.Status, Status = Enum(
Drivable.Status,
PREPARED=150, PREPARED=150,
PREPARING=340, PREPARING=340,
RAMPING=370, RAMPING=370,
@ -615,25 +565,16 @@ class Field(PpmsMixin, Drivable):
PersistentMode = Enum('PersistentMode', persistent=0, driven=1) PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
parameters = { value = Parameter(datatype=FloatRange(unit='T'), poll=True)
'value': status = Parameter(datatype=StatusType(Status), poll=True)
Override(datatype=FloatRange(unit='T'), poll=True), target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
'status': ramp = Parameter('ramping speed', readonly=False, handler=field,
Override(datatype=StatusType(Status), poll=True), datatype=FloatRange(0.064, 1.19, unit='T/min'))
'target': approachmode = Parameter('how to approach target', readonly=False, handler=field,
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field), datatype=EnumType(ApproachMode))
'ramp': persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
Parameter('ramping speed', readonly=False, handler=field, datatype=EnumType(PersistentMode))
datatype=FloatRange(0.064, 1.19, unit='T/min')), pollinterval = Parameter(visibility=3)
'approachmode':
Parameter('how to approach target', readonly=False, handler=field,
datatype=EnumType(ApproachMode)),
'persistentmode':
Parameter('what to do after changing field', readonly=False, handler=field,
datatype=EnumType(PersistentMode)),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'persistent mode'), 1: (Status.IDLE, 'persistent mode'),
@ -735,7 +676,7 @@ class Field(PpmsMixin, Drivable):
return Done return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current return None # do not execute FIELD command, as this would trigger a ramp up of leads current
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)
@ -751,20 +692,15 @@ class Position(PpmsMixin, Drivable):
move = IOHandler('move', 'MOVE?', '%g,%g,%g') move = IOHandler('move', 'MOVE?', '%g,%g,%g')
Status = Drivable.Status Status = Drivable.Status
parameters = {
'value': value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
Override(datatype=FloatRange(unit='deg'), poll=True), target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
'target': enabled = Parameter('is this channel used?', readonly=False, poll=False,
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move), datatype=BoolType(), default=True)
'enabled': speed = Parameter('motor speed', readonly=False, handler=move,
Parameter('is this channel used?', readonly=False, poll=False, datatype=FloatRange(0.8, 12, unit='deg/sec'))
datatype=BoolType(), default=True), pollinterval = Parameter(visibility=3)
'speed':
Parameter('motor speed', readonly=False, handler=move,
datatype=FloatRange(0.8, 12, unit='deg/sec')),
'pollinterval':
Override(visibility=3),
}
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'at target'), 1: (Status.IDLE, 'at target'),
5: (Status.BUSY, 'moving'), 5: (Status.BUSY, 'moving'),
@ -843,7 +779,7 @@ class Position(PpmsMixin, Drivable):
self.speed = value self.speed = value
return None # do not execute MOVE command, as this would trigger an unnecessary move return None # do not execute MOVE command, as this would trigger an unnecessary move
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)

View File

@ -26,7 +26,7 @@ import math
import numpy as np import numpy as np
from scipy.interpolate import splrep, splev # pylint: disable=import-error from scipy.interpolate import splrep, splev # pylint: disable=import-error
from secop.core import Readable, Parameter, Override, Attached, StringType, BoolType from secop.core import Readable, Parameter, Attached, StringType, BoolType
def linear(x): def linear(x):
@ -102,6 +102,7 @@ class CalCurve:
sensopt = calibspec.split(',') sensopt = calibspec.split(',')
calibname = sensopt.pop(0) calibname = sensopt.pop(0)
_, dot, ext = basename(calibname).rpartition('.') _, dot, ext = basename(calibname).rpartition('.')
kind = None
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','): for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
# first try without adding kind # first try without adding kind
filename = join(path.strip(), calibname) filename = join(path.strip(), calibname)
@ -150,16 +151,14 @@ class CalCurve:
class Sensor(Readable): class Sensor(Readable):
properties = { rawsensor = Attached()
'rawsensor': Attached(),
} calib = Parameter('calibration name', datatype=StringType(), readonly=False)
parameters = { abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
'calib': Parameter('calibration name', datatype=StringType(), readonly=False), value = Parameter(unit='K')
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True), pollinterval = Parameter(export=False)
'value': Override(unit='K'), status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
'pollinterval': Override(export=False),
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
}
pollerClass = None pollerClass = None
description = 'a calibrated sensor value' description = 'a calibrated sensor value'
_value_error = None _value_error = None

View File

@ -25,7 +25,7 @@
# no fixtures needed # no fixtures needed
import pytest import pytest
from secop.datatypes import ArrayOf, BLOBType, BoolType, \ from secop.datatypes import ArrayOf, BLOBType, BoolType, Enum, StatusType, \
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \ DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
@ -359,6 +359,7 @@ def test_BoolType():
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
BoolType(unit='K') BoolType(unit='K')
def test_ArrayOf(): def test_ArrayOf():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -478,6 +479,14 @@ def test_Command():
'result':{'type': 'int', 'min':-3, 'max':3}} 'result':{'type': 'int', 'min':-3, 'max':3}}
def test_StatusType():
status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400)
dt = StatusType(status_codes)
assert dt.IDLE == status_codes.IDLE
assert dt.ERROR == status_codes.ERROR
assert dt._enum == status_codes
def test_get_datatype(): def test_get_datatype():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(1) get_datatype(1)

View File

@ -107,15 +107,11 @@ def test_IOHandler():
class Module1(Module): class Module1(Module):
properties = { channel = Property('the channel', IntRange(), default=3)
'channel': Property('the channel', IntRange(), default=3), loop = Property('the loop', IntRange(), default=2)
'loop': Property('the loop', IntRange(), default=2), simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
} real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
parameters = { text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False),
'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False),
}
def sendRecv(self, command): def sendRecv(self, command):
assert data.pop('command') == command assert data.pop('command') == command
@ -196,6 +192,4 @@ def test_IOHandler():
with pytest.raises(ProgrammingError): # can not use a handler for different modules with pytest.raises(ProgrammingError): # can not use a handler for different modules
# pylint: disable=unused-variable # pylint: disable=unused-variable
class Module2(Module): class Module2(Module):
parameters = { simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
}

View File

@ -22,14 +22,14 @@
# ***************************************************************************** # *****************************************************************************
"""test data types.""" """test data types."""
# no fixtures needed
#import pytest
import threading import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType from secop.datatypes import BoolType, FloatRange, StringType
from secop.modules import Communicator, Drivable, Module from secop.modules import Communicator, Drivable, Module
from secop.params import Command, Override, Parameter, usercommand from secop.params import Command, Parameter
from secop.poller import BasicPoller from secop.poller import BasicPoller
from secop.errors import ProgrammingError
class DispatcherStub: class DispatcherStub:
@ -64,30 +64,27 @@ def test_Communicator():
assert event.is_set() # event should be set immediately assert event.is_set() # event should be set immediately
def test_ModuleMeta(): def test_ModuleMagic():
class Newclass1(Drivable): class Newclass1(Drivable):
parameters = { param1 = Parameter('param1', datatype=BoolType(), default=False)
'pollinterval': Override(reorder=True), param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
'param1' : Parameter('param1', datatype=BoolType(), default=False),
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True), @Command(argument=BoolType(), result=BoolType())
"cmd": Command('stuff', argument=BoolType(), result=BoolType()) def cmd(self, arg):
} """stuff"""
commands = { return not arg
# intermixing parameters with commands is not recommended,
# but acceptable for influencing the order a1 = Parameter('a1', datatype=BoolType(), default=False)
'a1': Parameter('a1', datatype=BoolType(), default=False), a2 = Parameter('a2', datatype=BoolType(), default=True)
'a2': Parameter('a2', datatype=BoolType(), default=True), value = Parameter(datatype=StringType(), default='first')
'value': Override(datatype=StringType(), default='first'),
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()), @Command(argument=BoolType(), result=BoolType())
} def cmd2(self, arg):
"""another stuff"""
return not arg
pollerClass = BasicPoller pollerClass = BasicPoller
def do_cmd(self, arg):
return not arg
def do_cmd2(self, arg):
return not arg
def read_param1(self): def read_param1(self):
return True return True
@ -103,19 +100,31 @@ def test_ModuleMeta():
def read_value(self): def read_value(self):
return 'second' return 'second'
with pytest.raises(ProgrammingError):
class Mod1(Module): # pylint: disable=unused-variable
def do_this(self): # old style command
pass
# first inherited accessibles, then Overrides with reorder=True and new accessibles with pytest.raises(ProgrammingError):
sortcheck1 = ['value', 'status', 'target', 'pollinterval', class Mod2(Module): # pylint: disable=unused-variable
param = Parameter(), # pylint: disable=trailing-comma-tuple
# first inherited accessibles
sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop',
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2'] 'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
class Newclass2(Newclass1): class Newclass2(Newclass1):
parameters = { paramOrder = 'param1', 'param2', 'cmd', 'value'
'cmd2': Override('another stuff'),
'value': Override(datatype=FloatRange(unit='deg'), reorder=True), @Command(description='another stuff')
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False), def cmd2(self, arg):
'b2': Parameter('<b2>', datatype=BoolType(), default=True, return arg
poll=True, readonly=False, initwrite=True),
} value = Parameter(datatype=FloatRange(unit='deg'))
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
def write_a1(self, value): def write_a1(self, value):
self._a1_written = value self._a1_written = value
@ -128,41 +137,9 @@ def test_ModuleMeta():
def read_value(self): def read_value(self):
return 0 return 0
sortcheck2 = ['status', 'target', 'pollinterval', # first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'value', 'a1', 'b2'] sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
# check consistency of new syntax:
class Testclass1(Drivable):
pollinterval = Parameter(reorder=True)
param1 = Parameter('param1', datatype=BoolType(), default=False)
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
@usercommand(BoolType(), BoolType())
def cmd(self, arg):
"""stuff"""
return not arg
a1 = Parameter('a1', datatype=BoolType(), default=False)
a2 = Parameter('a2', datatype=BoolType(), default=True)
value = Parameter(datatype=StringType(), default='first')
@usercommand(BoolType(), BoolType())
def cmd2(self, arg):
"""another stuff"""
return not arg
class Testclass2(Testclass1):
cmd2 = Command('another stuff')
value = Parameter(datatype=FloatRange(unit='deg'), reorder=True)
a1 = Parameter(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
for old, new in (Newclass1, Testclass1), (Newclass2, Testclass2):
assert len(old.accessibles) == len(new.accessibles)
for (oname, oobj), (nname, nobj) in zip(old.accessibles.items(), new.accessibles.items()):
assert oname == nname
assert oobj.for_export() == nobj.for_export()
logger = LoggerStub() logger = LoggerStub()
updates = {} updates = {}
@ -176,15 +153,11 @@ def test_ModuleMeta():
o2 = newclass('o2', logger, {'.description':''}, srv) o2 = newclass('o2', logger, {'.description':''}, srv)
for obj in [o1, o2]: for obj in [o1, o2]:
objects.append(obj) objects.append(obj)
ctr_found = set() for o in obj.accessibles.values():
for n, o in obj.accessibles.items():
# check that instance accessibles are unique objects # check that instance accessibles are unique objects
assert o not in params_found assert o not in params_found
params_found.add(o) params_found.add(o)
assert o.ctr not in ctr_found assert list(obj.accessibles) == sortcheck
ctr_found.add(o.ctr)
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
assert check_order == sorted(check_order)
# check for inital updates working properly # check for inital updates working properly
o1 = Newclass1('o1', logger, {'.description':''}, srv) o1 = Newclass1('o1', logger, {'.description':''}, srv)
@ -246,7 +219,7 @@ def test_ModuleMeta():
assert acs is not None assert acs is not None
else: # do not check object or mixin else: # do not check object or mixin
acs = {} acs = {}
for n, o in acs.items(): for o in acs.values():
# check that class accessibles are not reused as instance accessibles # check that class accessibles are not reused as instance accessibles
assert o not in params_found assert o not in params_found

View File

@ -25,68 +25,78 @@
# no fixtures needed # no fixtures needed
import pytest import pytest
from secop.datatypes import BoolType, IntRange from secop.datatypes import BoolType, IntRange, FloatRange
from secop.params import Command, Override, Parameter, Parameters from secop.params import Command, Parameter
from secop.modules import HasAccessibles
from secop.errors import ProgrammingError from secop.errors import ProgrammingError
def test_Command(): def test_Command():
cmd = Command('do_something') class Mod(HasAccessibles):
assert cmd.description == 'do_something' @Command()
assert cmd.ctr def cmd(self):
assert cmd.argument is None """do something"""
assert cmd.result is None @Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
assert cmd.for_export() == {'datainfo': {'type': 'command'}, def cmd2(self):
'description': 'do_something'} pass
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1)) assert Mod.cmd.description == 'do something'
assert cmd.description assert Mod.cmd.argument is None
assert isinstance(cmd.argument, IntRange) assert Mod.cmd.result is None
assert isinstance(cmd.result, IntRange) assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9}, 'description': 'do something'}
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}}, 'result': {'type': 'int', 'min': -1, 'max': 1}},
'description': 'do_something'} 'description': 'do some other thing'}
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9}, assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
'result': {'type': 'int', 'max': 1, 'min': -1}}, 'result': {'type': 'int', 'max': 1, 'min': -1}},
'description': 'do_something'} 'description': 'do some other thing'}
def test_Parameter(): def test_Parameter():
p1 = Parameter('description1', datatype=IntRange(), default=0) class Mod(HasAccessibles):
p2 = Parameter('description2', datatype=IntRange(), constant=1) p1 = Parameter('desc1', datatype=FloatRange(), default=0)
assert p1 != p2 p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
assert p1.ctr != p2.ctr p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False)
p4 = Parameter('desc4', datatype=FloatRange(), constant=1)
assert repr(Mod.p1) != repr(Mod.p3)
assert id(Mod.p1.datatype) != id(Mod.p2.datatype)
assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True}
assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True}
assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False}
assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True,
'constant': 1.0}
p3 = Mod.p1.copy()
assert id(p3) != id(Mod.p1)
assert repr(Mod.p1) == repr(p3)
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
Parameter(None, datatype=float, inherit=False) Parameter(None, datatype=float, inherit=False)
p3 = p1.copy()
assert p1.ctr == p3.ctr
p3.ctr = p1.ctr # manipulate ctr for next line
assert repr(p1) == repr(p3)
assert p1.datatype != p2.datatype
def test_Override(): def test_Override():
p = Parameter('description1', datatype=BoolType, default=False) class Base(HasAccessibles):
p1 = Parameter('description1', datatype=BoolType, default=False)
p2 = Parameter('description1', datatype=BoolType, default=False)
p3 = Parameter('description1', datatype=BoolType, default=False)
o = Override(default=True, reorder=True) class Mod(Base):
q = o.apply(p) p1 = Parameter(default=True)
qctr = q.ctr p2 = Parameter() # override without change
assert q.ctr > p.ctr # reorder=True: take ctr from override object
assert q != p
assert qctr == o.apply(p).ctr # do not create a new ctr when applied again
o2 = Override(default=True) assert Mod.p1 != Base.p1
q2 = o2.apply(p) assert Mod.p2 != Base.p2
assert q2.ctr == p.ctr # reorder=False: take ctr from inherited param assert Mod.p3 == Base.p3
assert q2 != p
assert repr(q2) != repr(p)
q3 = Override().apply(p) # Override without change assert id(Mod.p2) != id(Base.p2) # must be a new object
assert id(q2) != id(p) # must be a new object assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
assert repr(q3) == repr(p) # but must be a clone
def test_Parameters(): def test_Export():
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True))) class Mod:
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True) param = Parameter('description1', datatype=BoolType, default=False)
assert ps['_p2'].export == '_p2' assert Mod.param.export == '_param'

View File

@ -24,38 +24,58 @@
import pytest import pytest
from secop.datatypes import IntRange, StringType, FloatRange, ValueType from secop.datatypes import IntRange, StringType, FloatRange, ValueType
from secop.errors import ProgrammingError, ConfigError from secop.errors import ProgrammingError, ConfigError, BadValueError
from secop.properties import Property, Properties, HasProperties from secop.properties import Property, HasProperties
# args are: datatype, default, extname, export, mandatory, settable
def Prop(*args, name=None, **kwds):
# collect the args for Property
return name, args, kwds
# Property(description, datatype, default, ...)
V_test_Property = [ V_test_Property = [
[(StringType(), 'default', 'extname', False, False), [Prop(StringType(), 'default', extname='extname', mandatory=False),
dict(default='default', extname='extname', export=True, mandatory=False)], dict(default='default', extname='extname', export=True, mandatory=False)
[(IntRange(), '42', '_extname', False, True), ],
dict(default=42, extname='_extname', export=True, mandatory=True)], [Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
[(IntRange(), '42', '_extname', True, False), dict(default=42, extname='_custom', export=True, mandatory=True),
dict(default=42, extname='_extname', export=True, mandatory=False)], ],
[(IntRange(), 42, '_extname', True, True), [Prop(IntRange(), '42', export=True, name='name'),
dict(default=42, extname='_extname', export=True, mandatory=True)], dict(default=42, extname='_name', export=True, mandatory=False)
[(IntRange(), 0, '', True, True), ],
dict(default=0, extname='', export=True, mandatory=True)], [Prop(IntRange(), 42, '_extname', mandatory=True),
[(IntRange(), 0, '', True, False), dict(default=42, extname='_extname', export=True, mandatory=True)
dict(default=0, extname='', export=True, mandatory=False)], ],
[(IntRange(), 0, '', False, True), [Prop(IntRange(), 0, export=True, mandatory=True),
dict(default=0, extname='', export=False, mandatory=True)], dict(default=0, extname='', export=True, mandatory=True)
[(IntRange(), 0, '', False, False), ],
dict(default=0, extname='', export=False, mandatory=False)], [Prop(IntRange(), 0, export=True, mandatory=False),
[(IntRange(), None, '', None), dict(default=0, extname='', export=True, mandatory=False)
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory ],
[(ValueType(), 1, '', False), [Prop(IntRange(), 0, export=False, mandatory=True),
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory dict(default=0, extname='', export=False, mandatory=True)
],
[Prop(IntRange(), 0, export=False, mandatory=False),
dict(default=0, extname='', export=False, mandatory=False)
],
[Prop(IntRange()),
dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory
],
[Prop(ValueType(), 1),
dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory
],
] ]
@pytest.mark.parametrize('args, check', V_test_Property) @pytest.mark.parametrize('propargs, check', V_test_Property)
def test_Property(args, check): def test_Property(propargs, check):
p = Property('', *args) name, args, kwds = propargs
p = Property('', *args, **kwds)
if name:
p.__set_name__(None, name)
result = {k: getattr(p, k) for k in check} result = {k: getattr(p, k) for k in check}
assert result == check assert result == check
def test_Property_basic(): def test_Property_basic():
with pytest.raises(TypeError): with pytest.raises(TypeError):
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
@ -67,47 +87,47 @@ def test_Property_basic():
Property('', 1) Property('', 1)
Property('', IntRange(), '42', 'extname', False, False) Property('', IntRange(), '42', 'extname', False, False)
def test_Properties(): def test_Properties():
p = Properties() class Cls(HasProperties):
with pytest.raises(ProgrammingError): aa = Property('', IntRange(0, 99), '42', export=True)
p[1] = 2 bb = Property('', IntRange(), 0, export=False)
p['a'] = Property('', IntRange(), '42', export=True)
assert p['a'].default == 42 assert Cls.aa.default == 42
assert p['a'].export is True assert Cls.aa.export is True
assert p['a'].extname == '_a' assert Cls.aa.extname == '_aa'
with pytest.raises(ProgrammingError):
p['a'] = 137 cc = Cls()
with pytest.raises(ProgrammingError): with pytest.raises(BadValueError):
del p[1] cc.aa = 137
with pytest.raises(ProgrammingError):
del p['a'] assert Cls.bb.default == 0
p['a'] = Property('', IntRange(), 0, export=False) assert Cls.bb.export is False
assert p['a'].default == 0 assert Cls.bb.extname == ''
assert p['a'].export is False
assert p['a'].extname == ''
class c(HasProperties): class c(HasProperties):
properties = { # properties
'a' : Property('', IntRange(), 1), a = Property('', IntRange(), 1)
}
class cl(c): class cl(c):
properties = { # properties
'a' : Property('', IntRange(), 3), a = Property('', IntRange(), 3)
'b' : Property('', FloatRange(), 3.14), b = Property('', FloatRange(), 3.14)
'minabc': Property('', IntRange(), 8), minabc = Property('', IntRange(), 8)
'maxabc': Property('', IntRange(), 9), maxabc = Property('', IntRange(), 9)
'minx': Property('', IntRange(), 2), minx = Property('', IntRange(), 2)
'maxy': Property('', IntRange(), 1), maxy = Property('', IntRange(), 1)
}
def test_HasProperties(): def test_HasProperties():
o = c() o = c()
assert o.properties['a'] == 1 assert o.a == 1
o = cl() o = cl()
assert o.properties['a'] == 3 assert o.a == 3
assert o.properties['b'] == 3.14 assert o.b == 3.14
def test_Property_checks(): def test_Property_checks():
o = c() o = c()
@ -119,6 +139,7 @@ def test_Property_checks():
with pytest.raises(ConfigError): with pytest.raises(ConfigError):
o.checkProperties() o.checkProperties()
def test_Property_override(): def test_Property_override():
o1 = c() o1 = c()
class co(c): class co(c):
@ -131,10 +152,10 @@ def test_Property_override():
class cx(c): # pylint: disable=unused-variable class cx(c): # pylint: disable=unused-variable
def a(self): def a(self):
pass pass
assert 'collides with method' in str(e.value) assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e: with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable class cz(c): # pylint: disable=unused-variable
a = 's' a = 's'
assert 'can not be set to' in str(e.value) assert 'can not set' in str(e.value)