rework EnumType to use better Enum's

unfortunately IntEnum can't be bent like we would need it (extensible).
So we had to write our own....

The members of the Enum still behave like ints, but also have
.name and .value attributes, should they be needed.

needed adoptions to correctly use (and test) the EnumType are included.

Change-Id: Ie019d2f449a244c4fab00554b6c6daaac8948b59
Reviewed-on: https://forge.frm2.tum.de/review/17843
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber 2018-04-26 16:29:09 +02:00
parent 927ca854a2
commit 574a66c65b
15 changed files with 644 additions and 298 deletions

View File

@ -41,8 +41,8 @@ except ImportError:
import mlzlog import mlzlog
from secop.datatypes import get_datatype from secop.datatypes import get_datatype, EnumType
from secop.lib import mkthread, formatException from secop.lib import mkthread, formatException, formatExtendedStack
from secop.lib.parsing import parse_time, format_time from secop.lib.parsing import parse_time, format_time
#from secop.protocol.encoding import ENCODERS #from secop.protocol.encoding import ENCODERS
#from secop.protocol.framing import FRAMERS #from secop.protocol.framing import FRAMERS
@ -226,6 +226,7 @@ class Client(object):
try: try:
self._inner_run() self._inner_run()
except Exception as err: except Exception as err:
print(formatExtendedStack())
self.log.exception(err) self.log.exception(err)
raise raise
@ -383,6 +384,11 @@ class Client(object):
for module, moduleData in self.describing_data['modules'].items(): for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items(): for parameter, parameterData in moduleData['parameters'].items():
datatype = get_datatype(parameterData['datatype']) datatype = get_datatype(parameterData['datatype'])
# *sigh* special handling for 'some' parameters....
if isinstance(datatype, EnumType):
datatype._enum.name = parameter
if parameter == 'status':
datatype.subtypes[0]._enum.name = 'status'
self.describing_data['modules'][module]['parameters'] \ self.describing_data['modules'][module]['parameters'] \
[parameter]['datatype'] = datatype [parameter]['datatype'] = datatype
for _cmdname, cmdData in moduleData['commands'].items(): for _cmdname, cmdData in moduleData['commands'].items():

View File

@ -21,17 +21,21 @@
# ***************************************************************************** # *****************************************************************************
"""Define validated data types.""" """Define validated data types."""
from __future__ import print_function
try: try:
# py2 # py2
unicode(u'') unicode
except NameError: except NameError:
# py3 # py3
unicode = str # pylint: disable=redefined-builtin unicode = str # pylint: disable=redefined-builtin
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from .errors import ProgrammingError, ParsingError from secop.lib.enum import Enum
from .parse import Parser from secop.errors import ProgrammingError, ParsingError
from secop.parse import Parser
Parser = Parser() Parser = Parser()
@ -181,64 +185,33 @@ class IntRange(DataType):
class EnumType(DataType): class EnumType(DataType):
as_json = [u'enum'] def __init__(self, enum_or_name='', **kwds):
self._enum = Enum(enum_or_name, **kwds)
def __init__(self, *args, **kwds): @property
# enum keys are ints! remember mapping from intvalue to 'name' def as_json(self):
self.entries = {} # maps ints to strings return [u'enum'] + [dict((m.name, m.value) for m in self._enum.members)]
num = 0
for arg in args:
if not isinstance(arg, (str, unicode)):
raise ValueError(u'EnumType entries MUST be strings!')
self.entries[num] = arg
num += 1
for k, v in list(kwds.items()):
v = int(v)
if v in self.entries:
raise ValueError(
u'keyword argument %r=%d is already assigned %r' %
(k, v, self.entries[v]))
self.entries[v] = unicode(k)
# if len(self.entries) == 0:
# raise ValueError('Empty enums ae not allowed!')
# also keep a mapping from name strings to numbers
self.reversed = {} # maps Strings to ints
for k, v in self.entries.items():
if v in self.reversed:
raise ValueError(u'Mapping for %r=%r is not Unique!' % (v, k))
self.reversed[v] = k
self.as_json = [u'enum', self.reversed.copy()]
def __repr__(self): def __repr__(self):
return u'EnumType(%s)' % u', '.join( return "EnumType(%r, %s" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
[u'%s=%d' % (v, k) for k, v in list(self.entries.items())])
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
if value in self.reversed: return int(self.validate(value))
return self.reversed[value]
if int(value) in self.entries:
return int(value)
raise ValueError(u'%r is not one of %s' %
(unicode(value), u', '.join(list(self.reversed.keys()))))
def import_value(self, value): def import_value(self, value):
"""returns a python object from serialisation""" """returns a python object from serialisation"""
# internally we store the key (which is a string) return self.validate(value)
return self.entries[int(value)]
def validate(self, value): def validate(self, value):
"""return the validated (internal) value or raise""" """return the validated (internal) value or raise"""
if value in self.reversed: try:
return self.reversed[value] return self._enum[value]
if int(value) in self.entries: except KeyError:
return int(value) raise ValueError('%r is not a member of enum %r' % (value, self._enum))
raise ValueError(u'%r is not one of %s' %
(unicode(value), u', '.join(map(unicode, self.entries))))
def from_string(self, text): def from_string(self, text):
value = text return self.validate(text)
return self.validate(value)
class BLOBType(DataType): class BLOBType(DataType):
@ -606,7 +579,7 @@ DATATYPES = dict(
string=lambda _max=None, _min=0: StringType(_max, _min), string=lambda _max=None, _min=0: StringType(_max, _min),
array=lambda subtype, _max=None, _min=0: ArrayOf(get_datatype(subtype), _max, _min), array=lambda subtype, _max=None, _min=0: ArrayOf(get_datatype(subtype), _max, _min),
tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)), tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)),
enum=lambda kwds: EnumType(**kwds), enum=lambda kwds: EnumType('', **kwds),
struct=lambda named_subtypes: StructOf( struct=lambda named_subtypes: StructOf(
**dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))), **dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))),
command=Command, command=Command,

View File

@ -211,12 +211,11 @@ class ReadableWidget(QWidget):
if self._is_enum: if self._is_enum:
self._map = {} # maps QT-idx to name/value self._map = {} # maps QT-idx to name/value
self._revmap = {} # maps value/name to QT-idx self._revmap = {} # maps value/name to QT-idx
for idx, (val, name) in enumerate( for idx, member in enumerate(datatype._enum.members):
sorted(datatype.entries.items())): self._map[idx] = member
self._map[idx] = (name, val) self._revmap[member.name] = idx
self._revmap[name] = idx self._revmap[member.value] = idx
self._revmap[val] = idx self.targetComboBox.addItem(member.name, member.value)
self.targetComboBox.addItem(name, val)
self._init_status_widgets() self._init_status_widgets()
self._init_current_widgets() self._init_current_widgets()
@ -298,7 +297,8 @@ class DrivableWidget(ReadableWidget):
def update_current(self, value, qualifiers=None): def update_current(self, value, qualifiers=None):
if self._is_enum: if self._is_enum:
self.currentLineEdit.setText(self._map[self._revmap[value]][0]) member = self._map[self._revmap[value]]
self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
else: else:
self.currentLineEdit.setText(str(value)) self.currentLineEdit.setText(str(value))
@ -333,5 +333,5 @@ class DrivableWidget(ReadableWidget):
self.target_go(self.targetLineEdit.text()) self.target_go(self.targetLineEdit.text())
@pyqtSlot(unicode) @pyqtSlot(unicode)
def on_targetComboBox_activated(self, stuff): def on_targetComboBox_activated(self, selection):
self.target_go(stuff) self.target_go(selection)

View File

@ -21,6 +21,8 @@
# #
# ***************************************************************************** # *****************************************************************************
from __future__ import print_function
try: try:
# py2 # py2
unicode(u'') unicode(u'')
@ -32,6 +34,7 @@ from secop.gui.qt import QWidget, QLabel, QPushButton as QButton, QLineEdit, \
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.datatypes import EnumType from secop.datatypes import EnumType
from secop.lib import formatExtendedStack
class ParameterWidget(QWidget): class ParameterWidget(QWidget):
@ -94,15 +97,12 @@ class EnumParameterWidget(GenericParameterWidget):
loadUi(self, 'parambuttons_select.ui') loadUi(self, 'parambuttons_select.ui')
# transfer allowed settings from datatype to comboBoxes # transfer allowed settings from datatype to comboBoxes
self._map = {} # maps index to enumstring self._map = {} # maps index to EnumMember
self._revmap = {} # maps enumstring to index self._revmap = {} # maps Enum.name + Enum.value to index
index = 0 for index, member in enumerate(self._datatype._enum.members):
for enumval, enumname in sorted(self._datatype.entries.items()): self.setComboBox.addItem(member.name, member.value)
self.setComboBox.addItem(enumname, enumval) self._map[index] = member
self._map[index] = (enumval, enumname) self._revmap[member.name] = self._revmap[member.value] = index
self._revmap[enumname] = index
self._revmap[enumval] = index
index += 1
if self._readonly: if self._readonly:
self.setLabel.setEnabled(False) self.setLabel.setEnabled(False)
self.setComboBox.setEnabled(False) self.setComboBox.setEnabled(False)
@ -115,19 +115,16 @@ class EnumParameterWidget(GenericParameterWidget):
@pyqtSlot() @pyqtSlot()
def on_setPushButton_clicked(self): def on_setPushButton_clicked(self):
_enumval, enumname = self._map[self.setComboBox.currentIndex()] member = self._map[self.setComboBox.currentIndex()]
self.setRequested.emit(self._module, self._paramcmd, enumname) self.setRequested.emit(self._module, self._paramcmd, member)
def updateValue(self, value): def updateValue(self, value):
try: try:
value = int(value) member = self._map[self._revmap[int(value)]]
except ValueError: self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
pass except Exception:
if value in self._revmap:
index = self._revmap[value]
self.currentLineEdit.setText('(%d): %s' % self._map[index])
else:
self.currentLineEdit.setText('undefined Value: %r' % value) self.currentLineEdit.setText('undefined Value: %r' % value)
print(formatExtendedStack())
class GenericCmdWidget(ParameterWidget): class GenericCmdWidget(ParameterWidget):

View File

@ -62,16 +62,14 @@ class EnumWidget(QComboBox):
self._map = {} self._map = {}
self._revmap = {} self._revmap = {}
for idx, (val, name) in enumerate(datatype.entries.items()): for idx, member in enumerate(datatype._enum.members):
self._map[idx] = (name, val) self._map[idx] = member
self._revmap[name] = idx self._revmap[member.name] = idx
self._revmap[val] = idx self._revmap[member.value] = idx
self.addItem(name, val) self.addItem(member.name, member.value)
# XXX: fill Combobox from datatype
def get_value(self): def get_value(self):
# XXX: return integer corresponding to the selected item return self._map[self.currentIndex()].value
return self._map[self.currentIndex()][1]
def set_value(self, value): def set_value(self, value):
self.setCurrentIndex(self._revmap[value]) self.setCurrentIndex(self._revmap[value])

View File

@ -49,6 +49,8 @@ CONFIG = {
} }
unset_value = object()
class lazy_property(object): class lazy_property(object):
"""A property that calculates its value only once.""" """A property that calculates its value only once."""

306
secop/lib/enum.py Executable file
View File

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2016 by the authors, see LICENSE
#
# 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>
#
# *****************************************************************************
"""Enum class"""
__ALL__ = ['Enum']
try:
text_type = unicode # Py2
except NameError:
text_type = str # Py3
class EnumMember(object):
"""represents one member of an Enum
has an int-type value and attributes 'name' and 'value'
"""
__slots__ = ['name', 'value', 'enum']
def __init__(self, enum, name, value):
if not isinstance(enum, Enum):
raise TypeError('1st Argument must be an instance of class Enum()')
self.value = int(value)
self.enum = enum
self.name = name or 'unnamed'
# to behave like an int for comparisons
def __cmp__(self, other):
if isinstance(other, EnumMember):
other = other.value
if isinstance(other, (str, unicode)):
if other in self.enum:
other = self.enum[other].value
try:
other = int(other)
except Exception:
#raise TypeError('%r can not be compared to %r!' %(other, self))
return -1 # XXX:!
if self.value < other:
return -1
elif self.value > other:
return 1
return 0
def __lt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
def __le__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
def __eq__(self, other):
if isinstance(other, (EnumMember)):
return other.value == self.value
if isinstance(other, (int, long)):
return other == self.value
# compare by name (for (in)equality only)
if isinstance(other, (str, unicode)):
if other in self.enum:
return self.name == other
return False
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
def __ne__(self, other):
return not self.__eq__(other)
def __ge__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
def __gt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
# to be useful in indexing
def __hash__(self):
return self.value.__hash__()
# be read-only (except during initialization)
def __setattr__(self, key, value):
if key in self.__slots__ and not getattr(self, 'name', None):
return object.__setattr__(self, key, value)
raise TypeError('Modifying EnumMember\'s is not allowed!')
# allow access to other EnumMembers (via the Enum)
def __getattr__(self, key):
enum = object.__getattribute__(self, 'enum')
if key in enum:
return enum[key]
return object.__getattribute__(self, key)
# be human readable (for debugging)
def __repr__(self):
return '<%s.%s (%d)>' % (self.enum.name, self.name, self.value)
# numeric operations: delegate to int. Do we really need any of those?
def __add__(self, other):
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
def __sub__(self, other):
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
def __mul__(self, other):
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
def __div__(self, other):
return self.value.__div__(other.value if isinstance(other, EnumMember) else other)
def __truediv__(self, other):
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
def __floordiv__(self, other):
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
def __mod__(self, other):
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
def __divmod__(self, other):
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
def __pow__(self, other, *args):
return self.value.__pow__(other, *args)
def __lshift__(self, other):
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
def __rshift__(self, other):
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
def __radd__(self, other):
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
def __rsub__(self, other):
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
def __rmul__(self, other):
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
def __rdiv__(self, other):
return self.value.__rdiv__(other.value if isinstance(other, EnumMember) else other)
def __rtruediv__(self, other):
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
def __rfloordiv__(self, other):
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
def __rmod__(self, other):
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
def __rdivmod__(self, other):
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
def __rpow__(self, other, *args):
return self.value.__rpow__(other, *args)
def __rlshift__(self, other):
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
def __rrshift__(self, other):
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
# logical operations
def __and__(self, other):
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
def __xor__(self, other):
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
def __or__(self, other):
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
def __rand__(self, other):
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
def __rxor__(self, other):
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
def __ror__(self, other):
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
# other stuff
def __neg__(self):
return self.value.__neg__()
def __pos__(self):
return self.value.__pos__()
def __abs__(self):
return self.value.__abs__()
def __invert__(self):
return self.value.__invert__()
def __int__(self):
return self.value.__int__()
def __long__(self):
return self.value.__long__()
def __float__(self):
return self.value.__float__()
#return NotImplemented # makes no sense
def __oct__(self):
return self.value.__oct__()
def __hex__(self):
return self.value.__hex__()
def __index__(self):
return self.value.__index__()
# note: we do not implement the __i*__ methods as they modify our value
# inplace and we want to have a const
def __forbidden__(self, *args):
raise TypeError('Operation is forbidden!')
__iadd__ = __isub__ = __imul__ = __idiv__ = __itruediv__ = __ifloordiv__ = \
__imod__ = __ipow__ = __ilshift__ = __irshift__ = __iand__ = \
__ixor__ = __ior__ = __forbidden__
class Enum(dict):
"""The Enum class
use instance of this like this:
>>> status = Enum('status', idle=1, busy=2, error=3)
you may create an extended Enum:
>>> moveable_status = Enum(status, alarm=5)
>>> yet_another_enum = Enum('X', dict(a=1, b=2), c=3)
last example 'extends' the definition given by the dict with c=3.
accessing the members:
>>> status['idle'] == status.idle == status('idle')
>>> status[1] == status.idle == status(1)
Each member can be used like an int, so:
>>> status.idle == 1 is True
>>> status.error +5
You can neither modify members nor Enums.
You only can create an extended Enum.
"""
name = ''
def __init__(self, name='', parent=None, **kwds):
super(Enum, self).__init__()
if isinstance(name, (dict, Enum)) and parent is None:
# swap if only parent is given as positional argument
name, parent = '', name
# parent may be dict, or Enum....
if not name:
if isinstance(parent, Enum):
# if name was not given, use that of the parent
# this means, an extended Enum behaves like the parent
# THIS MAY BE CONFUSING SOMETIMES!
name=parent.name
# else:
# raise TypeError('Enum instances need a name or an Enum parent!')
if not isinstance(name, (str, text_type)):
raise TypeError('1st argument to Enum must be a name or an Enum!')
names = set()
values = set()
# pylint: disable=dangerous-default-value
def add(self, k, v, names = names, value = values):
"""helper for creating the enum members"""
if v is None:
# sugar: take the next free number if value was None
v = max(values or [0]) + 1
# sugar: if value is a name of another member,
# auto-assign the smallest free number which is bigger
# then that assigned to that name
if v in names:
v = self[v].value
while v in values:
v +=1
# check that the value is an int
_v = int(v)
if _v != v:
raise TypeError('Values must be integers!')
v = _v
# check for duplicates
if k in names:
raise TypeError('duplicate name %r' % k)
if v in values:
raise TypeError('duplicate value %d (key=%r)' % (v, k))
# remember it
self[v] = self[k] = EnumMember(self, k, v)
names.add(k)
values.add(v)
if isinstance(parent, Enum):
for m in parent.members:
add(self, m.name, m.value)
elif isinstance(parent, dict):
for k, v in parent.items():
add(self, k, v)
elif parent != None:
raise TypeError('parent (if given) MUST be a dict or an Enum!')
for k, v in kwds.items():
add(self, k, v)
self.members = tuple(sorted(self[n] for n in names))
self.name = name
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
if self.name:
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setattr__(key, value)
def __setitem__(self, key, value):
if self.name:
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setitem__(key, value)
def __delitem__(self, key):
raise TypeError('Enum %r can not be changed!' % self.name)
def __repr__(self):
return '<Enum %r (%d values)>' % (self.name, len(self)/2)
def __call__(self, key):
return self[key]

View File

@ -52,10 +52,10 @@ except ImportError:
return wrapper return wrapper
from secop.lib import formatExtendedStack, mkthread from secop.lib import formatExtendedStack, mkthread, unset_value
from secop.lib.enum import Enum
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
from secop.errors import ConfigError, ProgrammingError from secop.errors import ConfigError, ProgrammingError
from secop.protocol import status
from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, get_datatype from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, get_datatype
@ -81,12 +81,14 @@ class Param(object):
def __init__(self, def __init__(self,
description, description,
datatype=None, datatype=None,
default=Ellipsis, default=unset_value,
unit=None, unit='',
readonly=True, readonly=True,
export=True, export=True,
group='', group='',
poll=False): poll=False,
value=unset_value,
timestamp=0):
if not isinstance(datatype, DataType): if not isinstance(datatype, DataType):
if issubclass(datatype, DataType): if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???) # goodie: make an instance from a class (forgotten ()???)
@ -115,15 +117,7 @@ class Param(object):
def copy(self): def copy(self):
# return a copy of ourselfs # return a copy of ourselfs
return Param(description=self.description, return Param(**self.__dict__)
datatype=self.datatype,
default=self.default,
unit=self.unit,
readonly=self.readonly,
export=self.export,
group=self.group,
poll=self.poll,
)
def as_dict(self, static_only=False): def as_dict(self, static_only=False):
# used for serialisation only # used for serialisation only
@ -147,7 +141,10 @@ class Param(object):
class Override(object): class Override(object):
"""Stores the overrides to ba applied to a Param
note: overrides are applied by the metaclass during class creating
"""
def __init__(self, **kwds): def __init__(self, **kwds):
self.kwds = kwds self.kwds = kwds
@ -167,9 +164,9 @@ class Override(object):
paramobj) paramobj)
# storage for Commands settings (description + call signature...)
class Command(object): class Command(object):
"""storage for Commands settings (description + call signature...)
"""
def __init__(self, description, arguments=None, result=None): def __init__(self, description, arguments=None, result=None):
# descriptive text for humans # descriptive text for humans
self.description = description self.description = description
@ -195,7 +192,14 @@ class Command(object):
# warning: MAGIC! # warning: MAGIC!
class ModuleMeta(type): class ModuleMeta(type):
"""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__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs) newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs: if '__constructed__' in attrs:
@ -219,6 +223,11 @@ class ModuleMeta(type):
for n, o in attrs.get('overrides', {}).items(): for n, o in attrs.get('overrides', {}).items():
newparams[n] = o.apply(newparams[n].copy()) newparams[n] = o.apply(newparams[n].copy())
# Check naming of EnumType
for k, v in newparams.items():
if isinstance(v.datatype, EnumType) and not v.datatype._enum.name:
v.datatype._enum.name = k
# check validity of Param entries # check validity of Param entries
for pname, pobj in newtype.parameters.items(): for pname, pobj in newtype.parameters.items():
# XXX: allow dicts for overriding certain aspects only. # XXX: allow dicts for overriding certain aspects only.
@ -284,7 +293,7 @@ class ModuleMeta(type):
pobj = self.parameters[pname] pobj = self.parameters[pname]
value = pobj.datatype.validate(value) value = pobj.datatype.validate(value)
pobj.timestamp = time.time() pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value): if (not EVENT_ONLY_ON_CHANGED_VALUES) or (value != pobj.value):
pobj.value = value pobj.value = value
# also send notification # also send notification
if self.parameters[pname].export: if self.parameters[pname].export:
@ -311,52 +320,61 @@ class ModuleMeta(type):
return newtype return newtype
# Basic module class
#
# within Modules, parameters should only be addressed as self.<pname>
# i.e. self.value, self.target etc...
# these are accesses to the cached version.
# they can also be written to
# (which auto-calls self.write_<pname> and generate an async update)
# if you want to 'update from the hardware', call self.read_<pname>
# the return value of this method will be used as the new cached value and
# be returned.
@add_metaclass(ModuleMeta) @add_metaclass(ModuleMeta)
class Module(object): class Module(object):
"""Basic Module, doesn't do much""" """Basic Module
ALL secop Modules derive from this
note: within Modules, parameters should only be addressed as self.<pname>
i.e. self.value, self.target etc...
these are accessing the cached version.
they can also be written to (which auto-calls self.write_<pname> and
generate an async update)
if you want to 'update from the hardware', call self.read_<pname>() instead
the return value of this method will be used as the new cached value and
be an async update sent automatically.
"""
# static properties, definitions in derived classes should overwrite earlier ones. # static properties, definitions in derived classes should overwrite earlier ones.
# how to configure some stuff which makes sense to take from configfile??? # note: properties don't change after startup and are usually filled
# with data from a cfg file...
# note: so far all properties are STRINGS
# note: only the properties defined here are allowed to be set in the cfg file
properties = { properties = {
'group': None, # some Modules may be grouped together 'group': None, # some Modules may be grouped together
'description': "Short description of this Module class and its functionality.",
'meaning': None, # XXX: ??? 'meaning': None, # XXX: ???
'priority': None, # XXX: ??? 'priority': None, # XXX: ???
'visibility': None, # XXX: ???? 'visibility': None, # XXX: ????
'description': "The manufacturer forgot to set a meaningful description. please nag him!",
# what else? # what else?
} }
# parameter and commands are auto-merged upon subclassing # properties, parameter and commands are auto-merged upon subclassing
# parameters = { parameters = {}
# 'description': Param('short description of this module and its function', datatype=StringType(), default='no specified'),
# }
commands = {} commands = {}
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher): def __init__(self, logger, cfgdict, modname, dispatcher):
# remember the dispatcher object (for the async callbacks) # remember the dispatcher object (for the async callbacks)
self.DISPATCHER = dispatcher self.DISPATCHER = dispatcher
self.log = logger self.log = logger
self.name = devname self.name = modname
# make local copies of parameter # make local copies of parameter objects
# they need to be individual per instance since we use them also
# to cache the current value + qualifiers...
params = {} params = {}
for k, v in list(self.parameters.items()): for k, v in list(self.parameters.items()):
params[k] = v.copy() params[k] = v.copy()
# do not re-use self.parameters as this is the same for all instances
self.parameters = params self.parameters = params
# make local copies of properties # make local copies of properties
props = {} props = {}
for k, v in list(self.properties.items()): for k, v in list(self.properties.items()):
props[k] = v props[k] = v
self.properties = props self.properties = props
# check and apply properties specified in cfgdict # check and apply properties specified in cfgdict
@ -365,21 +383,21 @@ class Module(object):
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.properties: if k[1:] in self.properties:
self.properties[k[1:]] = v self.properties[k[1:]] = cfgdict.pop(k)
del cfgdict[k] else:
raise ConfigError('Module %r has no property %r' %
(self.name, k[1:]))
# remove unset (default) module properties
for k, v in list(self.properties.items()): # keep list() as dict may change during iter
if v is None:
del self.properties[k]
# derive automatic properties # MAGIC: derive 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.properties['_implementation'] = myclassname
self.properties['interface_class'] = [ self.properties['interface_class'] = [
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')]
#self.properties['interface'] = self.properties['interfaces'][0]
# remove unset (default) module properties
for k, v in list(self.properties.items()): # keep list() as dict may change during iter
if v is None:
del self.properties[k]
# check and apply parameter_properties # check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>' # specified as '<paramname>.<propertyname> = <propertyvalue>'
@ -391,11 +409,10 @@ class Module(object):
if propname == 'datatype': if propname == 'datatype':
paramobj.datatype = get_datatype(cfgdict.pop(k)) paramobj.datatype = get_datatype(cfgdict.pop(k))
elif hasattr(paramobj, propname): elif hasattr(paramobj, propname):
setattr(paramobj, propname, v) setattr(paramobj, propname, cfgdict.pop(k))
del cfgdict[k]
# check config for problems # check config for problems
# only accept config items specified in parameters # only accept remaining config items specified in parameters
for k, v in cfgdict.items(): for k, v in cfgdict.items():
if k not in self.parameters: if k not in self.parameters:
raise ConfigError( raise ConfigError(
@ -407,8 +424,8 @@ class Module(object):
# is not specified in cfgdict # is not specified in cfgdict
for k, v in self.parameters.items(): for k, v in self.parameters.items():
if k not in cfgdict: if k not in cfgdict:
if v.default is Ellipsis and k != 'value': if v.default is unset_value and k != 'value':
# Ellipsis is the one single value you can not specify.... # unset_value is the one single value you can not specify....
raise ConfigError('Module %s: Parameter %r has no default ' raise ConfigError('Module %s: Parameter %r has no default '
'value and was not given in config!' % 'value and was not given in config!' %
(self.name, k)) (self.name, k))
@ -416,7 +433,7 @@ class Module(object):
cfgdict[k] = v.default cfgdict[k] = v.default
# replace CLASS level Param objects with INSTANCE level ones # replace CLASS level Param objects with INSTANCE level ones
self.parameters[k] = self.parameters[k].copy() # self.parameters[k] = self.parameters[k].copy() # already done above...
# now 'apply' config: # now 'apply' config:
# pass values through the datatypes and store as attributes # pass values through the datatypes and store as attributes
@ -425,8 +442,6 @@ class Module(object):
continue continue
# apply datatype, complain if type does not fit # apply datatype, complain if type does not fit
datatype = self.parameters[k].datatype datatype = self.parameters[k].datatype
if datatype is not None:
# only check if datatype given
try: try:
v = datatype.validate(v) v = datatype.validate(v)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -434,6 +449,8 @@ class Module(object):
raise raise
# raise ConfigError('Module %s: config parameter %r:\n%r' % # raise ConfigError('Module %s: config parameter %r:\n%r' %
# (self.name, k, e)) # (self.name, k, e))
# note: this will call write_* methods which will
# write to the hardware, if possible!
setattr(self, k, v) setattr(self, k, v)
def init(self): def init(self):
@ -450,22 +467,25 @@ class Readable(Module):
"""Basic readable Module """Basic readable Module
providing the readonly parameter 'value' and 'status' providing the readonly parameter 'value' and 'status'
Also allow configurable polling per 'pollinterval' parameter.
""" """
# pylint: disable=invalid-name
Status = Enum('Status',
IDLE = 100,
WARN = 200,
UNSTABLE = 250,
ERROR = 400,
UNKNOWN = 900,
)
parameters = { parameters = {
'value': Param('current value of the Module', readonly=True, default=0., 'value': Param('current value of the Module', readonly=True, default=0.,
datatype=FloatRange(), unit='', poll=True), datatype=FloatRange(), unit='', poll=True),
'pollinterval': Param('sleeptime between polls', default=5, 'pollinterval': Param('sleeptime between polls', default=5,
readonly=False, datatype=FloatRange(0.1, 120), ), readonly=False, datatype=FloatRange(0.1, 120), ),
'status': Param('current status of the Module', default=(status.OK, ''), 'status': Param('current status of the Module',
datatype=TupleOf( default=(Status.IDLE, ''),
EnumType(**{ datatype=TupleOf(EnumType(Status), StringType()),
'IDLE': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN
}), StringType()),
readonly=True, poll=True), readonly=True, poll=True),
} }
@ -530,11 +550,16 @@ class Drivable(Writable):
Also status gets extended with a BUSY state indicating a running action. Also status gets extended with a BUSY state indicating a running action.
""" """
Status = Enum(Readable.Status, BUSY=300)
overrides = {
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
}
# improved polling: may poll faster if module is BUSY # improved polling: may poll faster if module is BUSY
def poll(self, nr=0): def poll(self, nr=0):
# poll status first # poll status first
stat = self.read_status(0) stat = self.read_status(0)
fastpoll = stat[0] == status.BUSY fastpoll = stat[0] == self.Status.BUSY
for pname, pobj in self.parameters.items(): for pname, pobj in self.parameters.items():
if not pobj.poll: if not pobj.poll:
continue continue

View File

@ -1,36 +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>
#
# *****************************************************************************
"""Define Status constants"""
# could also be some objects
OK = 100
WARN = 200
UNSTABLE = 250
BUSY = 300
ERROR = 400
UNKNOWN = -1
#OK = 'idle'
#BUSY = 'busy'
#WARN = 'alarm'
#UNSTABLE = 'unstable'
#ERROR = 'ERROR'
#UNKNOWN = 'unknown'

View File

@ -25,7 +25,6 @@ import time
import random import random
from secop.modules import Drivable, Command, Param from secop.modules import Drivable, Command, Param
from secop.protocol import status
from secop.datatypes import FloatRange, EnumType, TupleOf from secop.datatypes import FloatRange, EnumType, TupleOf
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
@ -100,7 +99,7 @@ class Cryostat(CryoBase):
group='pid', group='pid',
), ),
mode=Param("mode of regulation", mode=Param("mode of regulation",
datatype=EnumType('ramp', 'pid', 'openloop'), datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
default='ramp', default='ramp',
readonly=False, readonly=False,
), ),
@ -153,7 +152,7 @@ class Cryostat(CryoBase):
return value return value
self.target = value self.target = value
# next read_status will see this status, until the loop updates it # next read_status will see this status, until the loop updates it
self.status = status.BUSY, 'new target set' self.status = self.Status.BUSY, 'new target set'
return value return value
def read_maxpower(self, maxage=0): def read_maxpower(self, maxage=0):
@ -209,13 +208,13 @@ class Cryostat(CryoBase):
def thread(self): def thread(self):
self.sampletemp = self.T_start self.sampletemp = self.T_start
self.regulationtemp = self.T_start self.regulationtemp = self.T_start
self.status = status.OK, '' self.status = self.Status.IDLE, ''
while not self._stopflag: while not self._stopflag:
try: try:
self.__sim() self.__sim()
except Exception as e: except Exception as e:
self.log.exception(e) self.log.exception(e)
self.status = status.ERROR, str(e) self.status = self.Status.ERROR, str(e)
def __sim(self): def __sim(self):
# complex thread handling: # complex thread handling:
@ -264,7 +263,7 @@ class Cryostat(CryoBase):
# b) see # b) see
# http://brettbeauregard.com/blog/2011/04/ # http://brettbeauregard.com/blog/2011/04/
# improving-the-beginners-pid-introduction/ # improving-the-beginners-pid-introduction/
if self.mode != 'openloop': if self.mode != self.mode.openloop:
# fix artefacts due to too big timesteps # fix artefacts due to too big timesteps
# actually i would prefer reducing looptime, but i have no # actually i would prefer reducing looptime, but i have no
# good idea on when to increase it back again # good idea on when to increase it back again
@ -328,7 +327,7 @@ class Cryostat(CryoBase):
lastmode = self.mode lastmode = self.mode
# c) # c)
if self.setpoint != self.target: if self.setpoint != self.target:
if self.ramp == 0: if self.ramp == 0 or self.mode == self.mode.enum.pid:
maxdelta = 10000 maxdelta = 10000
else: else:
maxdelta = self.ramp / 60. * h maxdelta = self.ramp / 60. * h
@ -354,12 +353,12 @@ class Cryostat(CryoBase):
if abs(_T - self.target) > deviation: if abs(_T - self.target) > deviation:
deviation = abs(_T - self.target) deviation = abs(_T - self.target)
if (len(window) < 3) or deviation > self.tolerance: if (len(window) < 3) or deviation > self.tolerance:
self.status = status.BUSY, 'unstable' self.status = self.Status.BUSY, 'unstable'
elif self.setpoint == self.target: elif self.setpoint == self.target:
self.status = status.OK, 'at target' self.status = self.Status.IDLE, 'at target'
damper -= (damper - 1) * 0.1 # max value for damper is 11 damper -= (damper - 1) * 0.1 # max value for damper is 11
else: else:
self.status = status.BUSY, 'ramping setpoint' self.status = self.Status.BUSY, 'ramping setpoint'
damper -= (damper - 1) * 0.05 damper -= (damper - 1) * 0.05
self.regulationtemp = round(regulation, 3) self.regulationtemp = round(regulation, 3)
self.sampletemp = round(sample, 3) self.sampletemp = round(sample, 3)

View File

@ -24,9 +24,9 @@ import time
import random import random
import threading import threading
from secop.lib.enum import Enum
from secop.modules import Readable, Drivable, Param from secop.modules import Readable, Drivable, Param
from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType
from secop.protocol import status
class Switch(Drivable): class Switch(Drivable):
@ -50,9 +50,6 @@ class Switch(Drivable):
), ),
} }
def init(self):
self._started = 0
def read_value(self, maxage=0): def read_value(self, maxage=0):
# could ask HW # could ask HW
# we just return the value of the target here. # we just return the value of the target here.
@ -65,7 +62,7 @@ class Switch(Drivable):
def write_target(self, value): def write_target(self, value):
# could tell HW # could tell HW
pass setattr(self, 'status', (self.Status.BUSY, 'switching %s' % value.name.upper()))
# note: setting self.target to the new value is done after this.... # note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here # note: we may also return the read-back value from the hw here
@ -73,8 +70,8 @@ class Switch(Drivable):
self.log.info("read status") self.log.info("read status")
info = self._update() info = self._update()
if self.target == self.value: if self.target == self.value:
return status.OK, '' return self.Status.IDLE, ''
return status.BUSY, info return self.Status.BUSY, info
def _update(self): def _update(self):
started = self.parameters['target'].timestamp started = self.parameters['target'].timestamp
@ -90,7 +87,7 @@ class Switch(Drivable):
info = 'is switched OFF' info = 'is switched OFF'
self.value = self.target self.value = self.target
if info: if info:
self.log.debug(info) self.log.info(info)
return info return info
@ -119,7 +116,7 @@ class MagneticField(Drivable):
} }
def init(self): def init(self):
self._state = 'idle' self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch) self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
_thread = threading.Thread(target=self._thread) _thread = threading.Thread(target=self._thread)
_thread.daemon = True _thread.daemon = True
@ -135,44 +132,45 @@ class MagneticField(Drivable):
# note: we may also return the read-back value from the hw here # note: we may also return the read-back value from the hw here
def read_status(self, maxage=0): def read_status(self, maxage=0):
return (status.OK, '') if self._state == 'idle' else (status.BUSY, if self._state == self._state.enum.idle:
self._state) return (self.Status.IDLE, '')
return (self.Status.BUSY, self._state.name)
def _thread(self): def _thread(self):
loopdelay = 1 loopdelay = 1
while True: while True:
ts = time.time() ts = time.time()
if self._state == 'idle': if self._state == self._state.enum.idle:
if self.target != self.value: if self.target != self.value:
self.log.debug('got new target -> switching heater on') self.log.debug('got new target -> switching heater on')
self._state = 'switch_on' self._state = self._state.enum.switch_on
self._heatswitch.write_target('on') self._heatswitch.write_target('on')
if self._state == 'switch_on': if self._state == self._state.enum.switch_on:
# wait until switch is on # wait until switch is on
if self._heatswitch.read_value() == 'on': if self._heatswitch.read_value() == 'on':
self.log.debug('heatswitch is on -> ramp to %.3f' % self.log.debug('heatswitch is on -> ramp to %.3f' %
self.target) self.target)
self._state = 'ramp' self._state = self._state.enum.ramp
if self._state == 'ramp': if self._state == self._state.enum.ramp:
if self.target == self.value: if self.target == self.value:
self.log.debug('at field! mode is %r' % self.mode) self.log.debug('at field! mode is %r' % self.mode)
if self.mode: if self.mode:
self.log.debug('at field -> switching heater off') self.log.debug('at field -> switching heater off')
self._state = 'switch_off' self._state = self._state.enum.switch_off
self._heatswitch.write_target('off') self._heatswitch.write_target('off')
else: else:
self.log.debug('at field -> hold') self.log.debug('at field -> hold')
self._state = 'idle' self._state = self._state.enum.idle
self.status = self.read_status() # push async self.read_status() # push async
else: else:
step = self.ramp * loopdelay / 60. step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step) step = max(min(self.target - self.value, step), -step)
self.value += step self.value += step
if self._state == 'switch_off': if self._state == self._state.enum.switch_off:
# wait until switch is off # wait until switch is off
if self._heatswitch.read_value() == 'off': if self._heatswitch.read_value() == 'off':
self.log.debug('heatswitch is off at %.3f' % self.value) self.log.debug('heatswitch is off at %.3f' % self.value)
self._state = 'idle' self._state = self._state.enum.idle
self.read_status() # update async self.read_status() # update async
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!')
@ -229,10 +227,10 @@ class SampleTemp(Drivable):
while True: while True:
ts = time.time() ts = time.time()
if self.value == self.target: if self.value == self.target:
if self.status != status.OK: if self.status[0] != self.Status.IDLE:
self.status = status.OK, '' self.status = self.Status.IDLE, ''
else: else:
self.status = status.BUSY, 'ramping' self.status = self.Status.BUSY, 'ramping'
step = self.ramp * loopdelay / 60. step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step) step = max(min(self.target - self.value, step), -step)
self.value += step self.value += step
@ -278,7 +276,7 @@ class Label(Readable):
mf_mode = dev_mf.mode mf_mode = dev_mf.mode
mf_val = dev_mf.value mf_val = dev_mf.value
mf_unit = dev_mf.parameters['value'].unit mf_unit = dev_mf.parameters['value'].unit
if mf_stat[0] == status.OK: if mf_stat[0] == self.Status.IDLE:
state = 'Persistent' if mf_mode else 'Non-persistent' state = 'Persistent' if mf_mode else 'Non-persistent'
else: else:
state = mf_stat[1] or 'ramping' state = mf_stat[1] or 'ramping'
@ -293,21 +291,24 @@ class DatatypesTest(Readable):
"""for demoing all datatypes """for demoing all datatypes
""" """
parameters = { parameters = {
'enum': Param( 'enum': Param('enum', datatype=EnumType(boo=None, faar=None, z=9),
'enum', datatype=EnumType( readonly=False, default=1),
'boo', 'faar', z=9), readonly=False, default=1), 'tupleof': Param( 'tupleof': Param('tuple of int, float and str',
'tuple of int, float and str', datatype=TupleOf( datatype=TupleOf(IntRange(), FloatRange(),
IntRange(), FloatRange(), StringType()), readonly=False, default=( StringType()),
1, 2.3, 'a')), 'arrayof': Param( readonly=False, default=(1, 2.3, 'a')),
'array: 2..3 times bool', datatype=ArrayOf( 'arrayof': Param('array: 2..3 times bool',
BoolType(), 2, 3), readonly=False, default=[ datatype=ArrayOf(BoolType(), 2, 3),
1, 0, 1]), 'intrange': Param( readonly=False, default=[1, 0, 1]),
'intrange', datatype=IntRange( 'intrange': Param('intrange', datatype=IntRange(2, 9),
2, 9), readonly=False, default=4), 'floatrange': Param( readonly=False, default=4),
'floatrange', datatype=FloatRange( 'floatrange': Param('floatrange', datatype=FloatRange(-1, 1),
-1, 1), readonly=False, default=0, ), 'struct': Param( readonly=False, default=0, ),
'struct(a=str, b=int, c=bool)', datatype=StructOf( 'struct': Param('struct(a=str, b=int, c=bool)',
a=StringType(), b=IntRange(), c=BoolType()), ), } datatype=StructOf(a=StringType(), b=IntRange(),
c=BoolType()),
),
}
class ArrayTest(Readable): class ArrayTest(Readable):

View File

@ -24,7 +24,6 @@ from __future__ import absolute_import
from secop.datatypes import EnumType, FloatRange, StringType from secop.datatypes import EnumType, FloatRange, StringType
from secop.modules import Readable, Drivable, Param from secop.modules import Readable, Drivable, Param
from secop.protocol import status
try: try:
from pvaccess import Channel # import EPIVSv4 functionallity, PV access from pvaccess import Channel # import EPIVSv4 functionallity, PV access
@ -112,9 +111,9 @@ class EpicsReadable(Readable):
# XXX: Hardware may have it's own idea about the status: how to obtain? # XXX: Hardware may have it's own idea about the status: how to obtain?
if self.status_pv != 'unset': if self.status_pv != 'unset':
# XXX: how to map an unknown type+value to an valid status ??? # XXX: how to map an unknown type+value to an valid status ???
return status.UNKNOWN, self._read_pv(self.status_pv) return Drivable.Status.UNKNOWN, self._read_pv(self.status_pv)
# status_pv is unset # status_pv is unset
return (status.OK, 'no pv set') return (Drivable.Status.IDLE, 'no pv set')
class EpicsDrivable(Drivable): class EpicsDrivable(Drivable):
@ -179,13 +178,11 @@ class EpicsDrivable(Drivable):
# XXX: Hardware may have it's own idea about the status: how to obtain? # XXX: Hardware may have it's own idea about the status: how to obtain?
if self.status_pv != 'unset': if self.status_pv != 'unset':
# XXX: how to map an unknown type+value to an valid status ??? # XXX: how to map an unknown type+value to an valid status ???
return status.UNKNOWN, self._read_pv(self.status_pv) return Drivable.Status.UNKNOWN, self._read_pv(self.status_pv)
# status_pv is unset, derive status from equality of value + target # status_pv is unset, derive status from equality of value + target
return ( if self.read_value() == self.read_target():
status.OK, return (Drivable.Status.OK, '')
'') if self.read_value() == self.read_target() else ( return (Drivable.Status.BUSY, 'Moving')
status.BUSY,
'Moving')
# """Temperature control loop""" # """Temperature control loop"""
@ -223,11 +220,9 @@ class EpicsTempCtrl(EpicsDrivable):
# XXX: comparison may need to collect a history to detect oscillations # XXX: comparison may need to collect a history to detect oscillations
at_target = abs(self.read_value(maxage) - self.read_target(maxage)) \ at_target = abs(self.read_value(maxage) - self.read_target(maxage)) \
<= self.tolerance <= self.tolerance
return ( if at_target:
status.OK, return (Drivable.Status.OK, 'at Target')
'at Target') if at_target else ( return (Drivable.Status.BUSY, 'Moving')
status.BUSY,
'Moving')
# TODO: add support for strings over epics pv # TODO: add support for strings over epics pv
# def read_heaterrange(self, maxage=0): # def read_heaterrange(self, maxage=0):

View File

@ -36,7 +36,6 @@ import threading
import PyTango import PyTango
from secop.lib import lazy_property from secop.lib import lazy_property
from secop.protocol import status
#from secop.parse import Parser #from secop.parse import Parser
from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \ from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \
ArrayOf, EnumType ArrayOf, EnumType
@ -175,11 +174,11 @@ class PyTangoDevice(Module):
} }
tango_status_mapping = { tango_status_mapping = {
PyTango.DevState.ON: status.OK, PyTango.DevState.ON: Drivable.Status.IDLE,
PyTango.DevState.ALARM: status.WARN, PyTango.DevState.ALARM: Drivable.Status.WARN,
PyTango.DevState.OFF: status.ERROR, PyTango.DevState.OFF: Drivable.Status.ERROR,
PyTango.DevState.FAULT: status.ERROR, PyTango.DevState.FAULT: Drivable.Status.ERROR,
PyTango.DevState.MOVING: status.BUSY, PyTango.DevState.MOVING: Drivable.Status.BUSY,
} }
@lazy_property @lazy_property
@ -225,7 +224,7 @@ class PyTangoDevice(Module):
def _hw_wait(self): def _hw_wait(self):
"""Wait until hardware status is not BUSY.""" """Wait until hardware status is not BUSY."""
while self.read_status(0)[0] == 'BUSY': while self.read_status(0)[0] == Drivable.Status.BUSY:
sleep(0.3) sleep(0.3)
def _getProperty(self, name, dev=None): def _getProperty(self, name, dev=None):
@ -363,7 +362,7 @@ class PyTangoDevice(Module):
tangoStatus = self._dev.Status() tangoStatus = self._dev.Status()
# Map status # Map status
myState = self.tango_status_mapping.get(tangoState, status.UNKNOWN) myState = self.tango_status_mapping.get(tangoState, Drivable.Status.UNKNOWN)
return (myState, tangoStatus) return (myState, tangoStatus)
@ -465,7 +464,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
if attrInfo.unit != 'No unit': if attrInfo.unit != 'No unit':
self.parameters['value'].unit = attrInfo.unit self.parameters['value'].unit = attrInfo.unit
def poll(self, nr): def poll(self, nr=0):
super(AnalogOutput, self).poll(nr) super(AnalogOutput, self).poll(nr)
while len(self._history) > 2: while len(self._history) > 2:
# if history would be too short, break # if history would be too short, break
@ -509,8 +508,10 @@ class AnalogOutput(PyTangoDevice, Drivable):
return super(AnalogOutput, self).read_status() return super(AnalogOutput, self).read_status()
if self._timeout: if self._timeout:
if self._timeout < currenttime(): if self._timeout < currenttime():
return status.UNSTABLE, 'timeout after waiting for stable value' return self.Status.UNSTABLE, 'timeout after waiting for stable value'
return (status.BUSY, 'Moving') if self._moving else (status.OK, 'stable') if self._moving:
return (self.Status.BUSY, 'moving')
return (self.Status.OK, 'stable')
@property @property
def absmin(self): def absmin(self):
@ -555,7 +556,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
return self._checkLimits(value) return self._checkLimits(value)
def write_target(self, value=FloatRange()): def write_target(self, value=FloatRange()):
if self.status[0] == 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.do_stop()
@ -576,7 +577,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
self.read_status(0) # poll our status to keep it updated self.read_status(0) # poll our status to keep it updated
def _hw_wait(self): def _hw_wait(self):
while super(AnalogOutput, self).read_status()[0] == status.BUSY: while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
sleep(0.3) sleep(0.3)
def do_stop(self): def do_stop(self):
@ -791,7 +792,7 @@ class NamedDigitalInput(DigitalInput):
super(NamedDigitalInput, self).init() super(NamedDigitalInput, self).init()
try: try:
# pylint: disable=eval-used # pylint: disable=eval-used
self.parameters['value'].datatype = EnumType(**eval(self.mapping)) self.parameters['value'].datatype = EnumType('value', **eval(self.mapping))
except Exception as e: except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e) raise ValueError('Illegal Value for mapping: %r' % e)
@ -858,16 +859,15 @@ class NamedDigitalOutput(DigitalOutput):
super(NamedDigitalOutput, self).init() super(NamedDigitalOutput, self).init()
try: try:
# pylint: disable=eval-used # pylint: disable=eval-used
self.parameters['value'].datatype = EnumType(**eval(self.mapping)) self.parameters['value'].datatype = EnumType('value', **eval(self.mapping))
# pylint: disable=eval-used # pylint: disable=eval-used
self.parameters['target'].datatype = EnumType(**eval(self.mapping)) self.parameters['target'].datatype = EnumType('target', **eval(self.mapping))
except Exception as e: except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e) raise ValueError('Illegal Value for mapping: %r' % e)
def write_target(self, value): def write_target(self, value):
# map from enum-str to integer value # map from enum-str to integer value
self._dev.value = self.parameters[ self._dev.value = int(value)
'target'].datatype.reversed.get(value, value)
self.read_value() self.read_value()

View File

@ -90,12 +90,12 @@ def test_IntRange():
def test_EnumType(): def test_EnumType():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): with pytest.raises(TypeError):
EnumType(1) EnumType(1)
with pytest.raises(ValueError): with pytest.raises(TypeError):
EnumType('a', b=0) EnumType(['b', 0])
dt = EnumType(a=3, c=7, stuff=1) dt = EnumType('dt', a=3, c=7, stuff=1)
assert dt.as_json == ['enum', dict(a=3, c=7, stuff=1)] assert dt.as_json == ['enum', dict(a=3, c=7, stuff=1)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -116,9 +116,9 @@ def test_EnumType():
assert dt.export_value('c') == 7 assert dt.export_value('c') == 7
assert dt.export_value('stuff') == 1 assert dt.export_value('stuff') == 1
assert dt.export_value(1) == 1 assert dt.export_value(1) == 1
assert dt.import_value(7) == 'c' assert dt.import_value('c') == 7
assert dt.import_value(3) == 'a' assert dt.import_value('a') == 3
assert dt.import_value(1) == 'stuff' assert dt.import_value('stuff') == 1
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt.export_value(2) dt.export_value(2)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -304,7 +304,7 @@ def test_get_datatype():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['enum']) get_datatype(['enum'])
assert isinstance(get_datatype(['enum', dict(a=-2.718)]), EnumType) assert isinstance(get_datatype(['enum', dict(a=-2)]), EnumType)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['enum', 10, -10]) get_datatype(['enum', 10, -10])

80
test/test_lib_enum.py Normal file
View File

@ -0,0 +1,80 @@
# -*- 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>
#
# *****************************************************************************
"""test Enum type."""
import sys
sys.path.insert(0, sys.path[0] + '/..')
# no fixtures needed
import pytest
from secop.lib.enum import EnumMember, Enum
def test_EnumMember():
with pytest.raises(TypeError):
a = EnumMember(None, 'name', 'value')
with pytest.raises(TypeError):
a = EnumMember(None, 'name', 1)
e1=Enum('X')
with pytest.raises(ValueError):
a = EnumMember(e1, 'a', 'value')
a = EnumMember(e1, 'a', 1)
with pytest.raises(TypeError):
a.value = 2
with pytest.raises(TypeError):
a.value += 2
with pytest.raises(TypeError):
a += 2
# this shall work
assert 2 == (a + 1) # pylint: disable=C0122
assert (a - 1) == 0
assert a
assert a + a
assert (2 - a) == 1
assert -a == -1 # numeric negation
assert ~a == -2 # bitmask like NOT
assert (a & 3) == 1
assert (a | 6) == 7
assert (a ^ 7) == 6
assert a < 2
assert a > 0
assert a != 3
assert a == 1
def test_Enum():
e1 = Enum('e1')
e2 = Enum('e2', e1, a=1, b=3)
e3 = Enum('e3', e2, c='a')
assert e3.c == 2
with pytest.raises(TypeError):
e2.c = 2
assert e3.a < e2.b
assert e2.b > e3.a
assert e3.c >= e2.a
assert e3.b <= e2.b