big rework to comply to current spec
- adapt to release(v2018-11-07) - remove duplicate errors.py - adapt tests Change-Id: I383bb571f9808c72b37c12fbe55042011c4c0084 Reviewed-on: https://forge.frm2.tum.de/review/19397 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
@ -40,9 +40,9 @@ timeout=900
|
||||
pollinterval.export=False
|
||||
|
||||
# some parameter grouping
|
||||
p.group='pid'
|
||||
i.group='pid'
|
||||
d.group='pid'
|
||||
p.group=pid
|
||||
i.group=pid
|
||||
d.group=pid
|
||||
|
||||
value.unit='K'
|
||||
value.unit=K
|
||||
|
||||
|
@ -47,7 +47,7 @@ from secop.lib.parsing import parse_time, format_time
|
||||
#from secop.protocol.encoding import ENCODERS
|
||||
#from secop.protocol.framing import FRAMERS
|
||||
#from secop.protocol.messages import *
|
||||
from secop.protocol.errors import EXCEPTIONS
|
||||
from secop.errors import EXCEPTIONS
|
||||
|
||||
|
||||
class TCPConnection(object):
|
||||
@ -344,25 +344,18 @@ class Client(object):
|
||||
def _getDescribingParameterData(self, module, parameter):
|
||||
return self._getDescribingModuleData(module)['accessibles'][parameter]
|
||||
|
||||
def _decode_list_to_ordereddict(self, data):
|
||||
# takes a list of 2*N <key>, <value> entries and
|
||||
# return an orderedDict from it
|
||||
result = OrderedDict()
|
||||
while len(data) > 1:
|
||||
key = data.pop(0)
|
||||
value = data.pop(0)
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102
|
||||
# take a dict and move all keys which are not in specialkeys
|
||||
# into a 'properties' subdict
|
||||
# specialkeys entries are converted from list to ordereddict
|
||||
result = {}
|
||||
for k in specialkeys:
|
||||
result[k] = self._decode_list_to_ordereddict(data.pop(k, []))
|
||||
result['properties'] = data
|
||||
return result
|
||||
try:
|
||||
result = {}
|
||||
for k in specialkeys:
|
||||
result[k] = OrderedDict(data.pop(k, []))
|
||||
result['properties'] = data
|
||||
return result
|
||||
except Exception as err:
|
||||
raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data))
|
||||
|
||||
def _issueDescribe(self):
|
||||
_, _, describing_data = self._communicate('describe')
|
||||
|
@ -33,7 +33,7 @@ except NameError:
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
from secop.lib.enum import Enum
|
||||
from secop.errors import ProgrammingError, ParsingError
|
||||
from secop.errors import ProgrammingError, ProtocolError
|
||||
from secop.parse import Parser
|
||||
|
||||
|
||||
@ -92,10 +92,7 @@ class FloatRange(DataType):
|
||||
self.max = None if maxval is None else float(maxval)
|
||||
# note: as we may compare to Inf all comparisons would be false
|
||||
if (self.min or float(u'-inf')) <= (self.max or float(u'+inf')):
|
||||
if minval is None and maxval is None:
|
||||
self.as_json = [u'double']
|
||||
else:
|
||||
self.as_json = [u'double', minval, maxval]
|
||||
self.as_json = [u'double', minval, maxval]
|
||||
else:
|
||||
raise ValueError(u'Max must be larger then min!')
|
||||
|
||||
@ -141,15 +138,14 @@ class FloatRange(DataType):
|
||||
class IntRange(DataType):
|
||||
"""Restricted int type"""
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
self.min = int(minval) if minval is not None else minval
|
||||
self.max = int(maxval) if maxval is not None else maxval
|
||||
if self.min is not None and self.max is not None and self.min > self.max:
|
||||
def __init__(self, minval=-16777216, maxval=16777216):
|
||||
self.min = int(minval)
|
||||
self.max = int(maxval)
|
||||
if self.min > self.max:
|
||||
raise ValueError(u'Max must be larger then min!')
|
||||
if self.min is None and self.max is None:
|
||||
self.as_json = [u'int']
|
||||
else:
|
||||
self.as_json = [u'int', self.min, self.max]
|
||||
if None in (self.min, self.max):
|
||||
raise ValueError(u'Limits can not be None!')
|
||||
self.as_json = [u'int', self.min, self.max]
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
@ -193,7 +189,7 @@ class EnumType(DataType):
|
||||
return [u'enum'] + [dict((m.name, m.value) for m in self._enum.members)]
|
||||
|
||||
def __repr__(self):
|
||||
return "EnumType(%r, %s" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
|
||||
return u"EnumType(%r, %s)" % (self._enum.name, ', '.join(u'%s=%d' %(m.name, m.value) for m in self._enum.members))
|
||||
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -208,7 +204,7 @@ class EnumType(DataType):
|
||||
try:
|
||||
return self._enum[value]
|
||||
except KeyError:
|
||||
raise ValueError('%r is not a member of enum %r' % (value, self._enum))
|
||||
raise ValueError(u'%r is not a member of enum %r' % (value, self._enum))
|
||||
|
||||
def from_string(self, text):
|
||||
return self.validate(text)
|
||||
@ -217,26 +213,21 @@ class EnumType(DataType):
|
||||
class BLOBType(DataType):
|
||||
minsize = None
|
||||
maxsize = None
|
||||
def __init__(self, maxsize=None, minsize=0):
|
||||
|
||||
def __init__(self, minsize=0, maxsize=None):
|
||||
# if only one argument is given, use exactly that many bytes
|
||||
# if nothing is given, default to 255
|
||||
if maxsize is None:
|
||||
raise ValueError(u'BLOBType needs a maximum number of Bytes count!')
|
||||
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize)
|
||||
self.minsize = minsize
|
||||
self.maxsize = maxsize
|
||||
if minsize < 0:
|
||||
maxsize = minsize or 255
|
||||
minsize = maxsize
|
||||
minsize = int(minsize)
|
||||
maxsize = int(maxsize)
|
||||
self.minsize, self.maxsize = min(minsize, maxsize), max(minsize, maxsize)
|
||||
if self.minsize < 0:
|
||||
raise ValueError(u'sizes must be bigger than or equal to 0!')
|
||||
if minsize:
|
||||
self.as_json = [u'blob', maxsize, minsize]
|
||||
else:
|
||||
self.as_json = [u'blob', maxsize]
|
||||
self.as_json = [u'blob', minsize, maxsize]
|
||||
|
||||
def __repr__(self):
|
||||
if self.minsize:
|
||||
return u'BLOB(%s, %s)' % (
|
||||
unicode(self.minsize) if self.minsize else u'unspecified',
|
||||
unicode(self.maxsize) if self.maxsize else u'unspecified')
|
||||
return u'BLOB(%s)' % (unicode(self.minsize) if self.minsize else u'unspecified')
|
||||
return u'BLOB(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
|
||||
|
||||
def validate(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
@ -271,25 +262,19 @@ class StringType(DataType):
|
||||
minsize = None
|
||||
maxsize = None
|
||||
|
||||
def __init__(self, maxsize=255, minsize=0):
|
||||
def __init__(self, minsize=0, maxsize=None):
|
||||
if maxsize is None:
|
||||
raise ValueError(u'StringType needs a maximum bytes count!')
|
||||
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize)
|
||||
|
||||
if minsize < 0:
|
||||
raise ValueError(u'sizes must be >= 0')
|
||||
if minsize:
|
||||
self.as_json = [u'string', maxsize, minsize]
|
||||
else:
|
||||
self.as_json = [u'string', maxsize]
|
||||
self.minsize = minsize
|
||||
self.maxsize = maxsize
|
||||
maxsize = minsize or 255
|
||||
minsize = 0
|
||||
minsize = int(minsize)
|
||||
maxsize = int(maxsize)
|
||||
self.minsize, self.maxsize = min(minsize, maxsize), max(minsize, maxsize)
|
||||
if self.minsize < 0:
|
||||
raise ValueError(u'sizes must be bigger than or equal to 0!')
|
||||
self.as_json = [u'string', minsize, maxsize]
|
||||
|
||||
def __repr__(self):
|
||||
if self.minsize:
|
||||
return u'StringType(%s, %s)' % (
|
||||
unicode(self.minsize) or u'unspecified', unicode(self.maxsize) or u'unspecified')
|
||||
return u'StringType(%s)' % unicode(self.maxsize)
|
||||
return u'StringType(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
|
||||
|
||||
def validate(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
@ -358,24 +343,27 @@ class BoolType(DataType):
|
||||
class ArrayOf(DataType):
|
||||
minsize = None
|
||||
maxsize = None
|
||||
def __init__(self, subtype, maxsize=None, minsize=0):
|
||||
self.subtype = subtype
|
||||
subtype = None
|
||||
def __init__(self, subtype, minsize=0, maxsize=None):
|
||||
# one argument -> exactly that size
|
||||
# argument default to 10
|
||||
if maxsize is None:
|
||||
maxsize = minsize or 10
|
||||
minsize = maxsize
|
||||
if not isinstance(subtype, DataType):
|
||||
raise ValueError(
|
||||
u'ArrayOf only works with DataType objs as first argument!')
|
||||
u'ArrayOf only works with a DataType as first argument!')
|
||||
self.subtype = subtype
|
||||
|
||||
if maxsize is None:
|
||||
raise ValueError(u'ArrayOf needs a maximum size')
|
||||
minsize = int(minsize)
|
||||
maxsize = int(maxsize)
|
||||
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize)
|
||||
if minsize < 0:
|
||||
raise ValueError(u'sizes must be > 0')
|
||||
if maxsize < 1:
|
||||
raise ValueError(u'Maximum size must be >= 1!')
|
||||
# if only one arg is given, it is maxsize!
|
||||
if minsize:
|
||||
self.as_json = [u'array', subtype.as_json, maxsize, minsize]
|
||||
else:
|
||||
self.as_json = [u'array', subtype.as_json, maxsize]
|
||||
self.as_json = [u'array', minsize, maxsize, subtype.as_json]
|
||||
self.minsize = minsize
|
||||
self.maxsize = maxsize
|
||||
|
||||
@ -410,7 +398,7 @@ class ArrayOf(DataType):
|
||||
def from_string(self, text):
|
||||
value, rem = Parser.parse(text)
|
||||
if rem:
|
||||
raise ParsingError(u'trailing garbage: %r' % rem)
|
||||
raise ProtocolError(u'trailing garbage: %r' % rem)
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
@ -424,7 +412,7 @@ class TupleOf(DataType):
|
||||
raise ValueError(
|
||||
u'TupleOf only works with DataType objs as arguments!')
|
||||
self.subtypes = subtypes
|
||||
self.as_json = [u'tuple', [subtype.as_json for subtype in subtypes]]
|
||||
self.as_json = [u'tuple'] + [subtype.as_json for subtype in subtypes]
|
||||
|
||||
def __repr__(self):
|
||||
return u'TupleOf(%s)' % u', '.join([repr(st) for st in self.subtypes])
|
||||
@ -454,7 +442,7 @@ class TupleOf(DataType):
|
||||
def from_string(self, text):
|
||||
value, rem = Parser.parse(text)
|
||||
if rem:
|
||||
raise ParsingError(u'trailing garbage: %r' % rem)
|
||||
raise ProtocolError(u'trailing garbage: %r' % rem)
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
@ -512,49 +500,40 @@ class StructOf(DataType):
|
||||
def from_string(self, text):
|
||||
value, rem = Parser.parse(text)
|
||||
if rem:
|
||||
raise ParsingError(u'trailing garbage: %r' % rem)
|
||||
raise ProtocolError(u'trailing garbage: %r' % rem)
|
||||
return self.validate(dict(value))
|
||||
|
||||
|
||||
class CommandType(DataType):
|
||||
IS_COMMAND = True
|
||||
argtype = None
|
||||
resulttype = None
|
||||
|
||||
def __init__(self, argtypes=tuple(), resulttype=None):
|
||||
for arg in argtypes:
|
||||
if not isinstance(arg, DataType):
|
||||
raise ValueError(u'CommandType: Argument types must be DataTypes!')
|
||||
def __init__(self, argtype=None, resulttype=None):
|
||||
if argtype is not None:
|
||||
if not isinstance(argtype, DataType):
|
||||
raise ValueError(u'CommandType: Argument type must be a DataType!')
|
||||
if resulttype is not None:
|
||||
if not isinstance(resulttype, DataType):
|
||||
raise ValueError(u'CommandType: result type must be DataTypes!')
|
||||
self.argtypes = argtypes
|
||||
raise ValueError(u'CommandType: Result type must be a DataType!')
|
||||
self.argtype = argtype
|
||||
self.resulttype = resulttype
|
||||
|
||||
if resulttype is not None:
|
||||
self.as_json = [u'command',
|
||||
[t.as_json for t in argtypes],
|
||||
resulttype.as_json]
|
||||
else:
|
||||
self.as_json = [u'command',
|
||||
[t.as_json for t in argtypes],
|
||||
None] # XXX: or NoneType ???
|
||||
if argtype:
|
||||
argtype = argtype.as_json
|
||||
if resulttype:
|
||||
resulttype = resulttype.as_json
|
||||
self.as_json = [u'command', argtype, resulttype]
|
||||
|
||||
def __repr__(self):
|
||||
argstr = u', '.join(repr(arg) for arg in self.argtypes)
|
||||
argstr = repr(self.argtype) if self.argtype else ''
|
||||
if self.resulttype is None:
|
||||
return u'CommandType(%s)' % argstr
|
||||
return u'CommandType(%s)->%s' % (argstr, repr(self.resulttype))
|
||||
|
||||
def validate(self, value):
|
||||
"""return the validated arguments value or raise"""
|
||||
try:
|
||||
if len(value) != len(self.argtypes):
|
||||
raise ValueError(
|
||||
u'Illegal number of Arguments! Need %d arguments.' %
|
||||
len(self.argtypes))
|
||||
# validate elements and return
|
||||
return [t.validate(v) for t, v in zip(self.argtypes, value)]
|
||||
except Exception as exc:
|
||||
raise ValueError(u'Can not validate %s: %s' % (repr(value), unicode(exc)))
|
||||
"""return the validated argument value or raise"""
|
||||
return self.argtype.validate(value)
|
||||
|
||||
def export_value(self, value):
|
||||
raise ProgrammingError(u'values of type command can not be transported!')
|
||||
@ -565,7 +544,7 @@ class CommandType(DataType):
|
||||
def from_string(self, text):
|
||||
value, rem = Parser.parse(text)
|
||||
if rem:
|
||||
raise ParsingError(u'trailing garbage: %r' % rem)
|
||||
raise ProtocolError(u'trailing garbage: %r' % rem)
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
@ -584,7 +563,7 @@ class LimitsType(StructOf):
|
||||
class Status(TupleOf):
|
||||
# shorten initialisation and allow acces to status enumMembers from status values
|
||||
def __init__(self, enum):
|
||||
TupleOf.__init__(self, EnumType(enum), StringType(255))
|
||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||
self.enum = enum
|
||||
def __getattr__(self, key):
|
||||
enum = TupleOf.__getattr__(self, 'enum')
|
||||
@ -595,17 +574,17 @@ class Status(TupleOf):
|
||||
|
||||
# XXX: derive from above classes automagically!
|
||||
DATATYPES = dict(
|
||||
bool=BoolType,
|
||||
int=lambda _min=None, _max=None: IntRange(_min, _max),
|
||||
double=lambda _min=None, _max=None: FloatRange(_min, _max),
|
||||
blob=lambda _max=None, _min=0: BLOBType(_max, _min),
|
||||
string=lambda _max=None, _min=0: StringType(_max, _min),
|
||||
array=lambda subtype, _max=None, _min=0: ArrayOf(get_datatype(subtype), _max, _min),
|
||||
tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)),
|
||||
enum=lambda kwds: EnumType('', **kwds),
|
||||
struct=lambda named_subtypes: StructOf(
|
||||
bool =BoolType,
|
||||
int =IntRange,
|
||||
double =FloatRange,
|
||||
blob =BLOBType,
|
||||
string =StringType,
|
||||
array =lambda _min, _max, subtype: ArrayOf(get_datatype(subtype), _min, _max),
|
||||
tuple =lambda *subtypes: TupleOf(*map(get_datatype, subtypes)),
|
||||
enum =lambda kwds: EnumType('', **kwds),
|
||||
struct =lambda named_subtypes: StructOf(
|
||||
**dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))),
|
||||
command=lambda args, res: CommandType(map(get_datatype, args), get_datatype(res)),
|
||||
command = lambda arg, res: CommandType(get_datatype(arg), get_datatype(res)),
|
||||
)
|
||||
|
||||
|
||||
@ -623,14 +602,10 @@ def get_datatype(json):
|
||||
if len(json) < 1:
|
||||
raise ValueError(u'can not validate %r' % json)
|
||||
base = json[0]
|
||||
args = []
|
||||
if len(json) > 1:
|
||||
args = json[1:]
|
||||
if base in DATATYPES:
|
||||
if base in (u'enum', u'struct'):
|
||||
if len(json) > 1:
|
||||
args = json[1:]
|
||||
else:
|
||||
args = []
|
||||
else:
|
||||
args = json[1:]
|
||||
try:
|
||||
return DATATYPES[base](*args)
|
||||
except (TypeError, AttributeError):
|
||||
|
115
secop/errors.py
115
secop/errors.py
@ -1,5 +1,6 @@
|
||||
# -*- 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
|
||||
@ -18,79 +19,117 @@
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""error class for our little framework"""
|
||||
|
||||
# base class
|
||||
class SECoPServerError(Exception):
|
||||
errorclass = 'InternalError'
|
||||
"""Define (internal) SECoP Errors"""
|
||||
|
||||
|
||||
# those errors should never be seen remotely!
|
||||
# just in case they are, these are flagged as InternalError
|
||||
class ConfigError(SECoPServerError):
|
||||
pass
|
||||
class SECoPError(RuntimeError):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
RuntimeError.__init__(self)
|
||||
self.args = args
|
||||
for k, v in list(kwds.items()):
|
||||
setattr(self, k, v)
|
||||
|
||||
def __repr__(self):
|
||||
args = ', '.join(map(repr, self.args))
|
||||
kwds = ', '.join(['%s=%r' % i for i in list(self.__dict__.items())])
|
||||
res = []
|
||||
if args:
|
||||
res.append(args)
|
||||
if kwds:
|
||||
res.append(kwds)
|
||||
return '%s(%s)' % (self.name, ', '.join(res))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__[:-len('Error')]
|
||||
|
||||
|
||||
class ProgrammingError(SECoPServerError):
|
||||
pass
|
||||
class SECoPServerError(SECoPError):
|
||||
name = 'InternalError'
|
||||
|
||||
|
||||
class ParsingError(SECoPServerError):
|
||||
pass
|
||||
class InternalError(SECoPError):
|
||||
name = 'InternalError'
|
||||
|
||||
|
||||
# to be exported for remote operation
|
||||
class SECoPError(SECoPServerError):
|
||||
pass
|
||||
class ProgrammingError(SECoPError):
|
||||
name = 'InternalError'
|
||||
|
||||
|
||||
class ConfigError(SECoPError):
|
||||
name = 'InternalError'
|
||||
|
||||
|
||||
class ProtocolError(SECoPError):
|
||||
name = 'ProtocolError'
|
||||
|
||||
|
||||
class NoSuchModuleError(SECoPError):
|
||||
errorclass = 'NoSuchModule'
|
||||
name = 'NoSuchModule'
|
||||
|
||||
|
||||
class NoSuchParameterError(SECoPError):
|
||||
errorclass = 'NoSuchParameter'
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchCommandError(SECoPError):
|
||||
errorclass = 'NoSuchCommand'
|
||||
|
||||
|
||||
class CommandFailedError(SECoPError):
|
||||
errorclass = 'CommandFailed'
|
||||
|
||||
|
||||
class CommandRunningError(SECoPError):
|
||||
errorclass = 'CommandRunning'
|
||||
pass
|
||||
|
||||
|
||||
class ReadOnlyError(SECoPError):
|
||||
errorclass = 'ReadOnly'
|
||||
pass
|
||||
|
||||
|
||||
class BadValueError(SECoPError):
|
||||
errorclass = 'BadValue'
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(SECoPError):
|
||||
errorclass = 'CommunicationFailed'
|
||||
class CommandFailedError(SECoPError):
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutError(SECoPError):
|
||||
errorclass = 'CommunicationFailed' # XXX: add to SECop messages
|
||||
class CommandRunningError(SECoPError):
|
||||
pass
|
||||
|
||||
|
||||
class HardwareError(SECoPError):
|
||||
errorclass = 'CommunicationFailed' # XXX: Add to SECoP messages
|
||||
class CommunicationFailedError(SECoPError):
|
||||
pass
|
||||
|
||||
|
||||
class IsBusyError(SECoPError):
|
||||
errorclass = 'IsBusy'
|
||||
pass
|
||||
|
||||
|
||||
class IsErrorError(SECoPError):
|
||||
errorclass = 'IsError'
|
||||
pass
|
||||
|
||||
|
||||
class DisabledError(SECoPError):
|
||||
errorclass = 'Disabled'
|
||||
pass
|
||||
|
||||
|
||||
class HardwareError(SECoPError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
EXCEPTIONS = dict(
|
||||
NoSuchModule=NoSuchModuleError,
|
||||
NoSuchParameter=NoSuchParameterError,
|
||||
NoSuchCommand=NoSuchCommandError,
|
||||
CommandFailed=CommandFailedError,
|
||||
CommandRunning=CommandRunningError,
|
||||
Readonly=ReadOnlyError,
|
||||
BadValue=BadValueError,
|
||||
CommunicationFailed=CommunicationFailedError,
|
||||
HardwareError=HardwareError,
|
||||
IsBusy=IsBusyError,
|
||||
IsError=IsErrorError,
|
||||
Disabled=DisabledError,
|
||||
SyntaxError=ProtocolError,
|
||||
InternalError=InternalError,
|
||||
# internal short versions (candidates for spec)
|
||||
Protocol=ProtocolError,
|
||||
Internal=InternalError,
|
||||
)
|
||||
|
@ -112,9 +112,9 @@ class HAS_Timeout(Feature):
|
||||
class HAS_Pause(Feature):
|
||||
# just a proposal, can't agree on it....
|
||||
accessibles = {
|
||||
'pause': Command('pauses movement', arguments=[], result=None),
|
||||
'pause': Command('pauses movement', argument=None, result=None),
|
||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
||||
arguments=[], result=None),
|
||||
argument=None, result=None),
|
||||
}
|
||||
|
||||
|
||||
|
430
secop/gui/miniplot.py
Normal file
430
secop/gui/miniplot.py
Normal file
@ -0,0 +1,430 @@
|
||||
# -*- 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from os import path
|
||||
|
||||
from secop.gui.qt import Qt, QColor, QWidget, QSize, \
|
||||
QBrush, QPainter, QPolygonF, QPointF, QPen, QRectF
|
||||
|
||||
|
||||
|
||||
_magenta = QBrush(QColor('#A12F86'))
|
||||
_yellow = QBrush(QColor('yellow'))
|
||||
_white = QBrush(QColor('white'))
|
||||
_lightgrey = QBrush(QColor('lightgrey'))
|
||||
_grey = QBrush(QColor('grey'))
|
||||
_darkgrey = QBrush(QColor('#404040'))
|
||||
_black = QBrush(QColor('black'))
|
||||
_blue = QBrush(QColor('blue'))
|
||||
_green = QBrush(QColor('green'))
|
||||
_red = QBrush(QColor('red'))
|
||||
_olive = QBrush(QColor('olive'))
|
||||
_orange = QBrush(QColor('#ffa500'))
|
||||
|
||||
|
||||
my_uipath = path.dirname(__file__)
|
||||
|
||||
class MiniPlotCurve(object):
|
||||
# placeholder for data
|
||||
linecolor = _black
|
||||
linewidth = 0 # set to 0 to disable lines
|
||||
symbolcolors = (_black, _white) # line, fill
|
||||
symbolsize = 3 # both symbol linewidth and symbolsize, set to 0 to disable
|
||||
errorbarcolor = _darkgrey
|
||||
errorbarwidth = 3 # set to 0 to disable errorbar
|
||||
|
||||
def __init__(self):
|
||||
self.data = [] # tripels of x, y, err (err may be None)
|
||||
|
||||
@property
|
||||
def xvalues(self):
|
||||
return [p[0] for p in self.data] if self.data else [0]
|
||||
|
||||
@property
|
||||
def yvalues(self):
|
||||
return [p[1] for p in self.data] if self.data else [0]
|
||||
|
||||
@property
|
||||
def errvalues(self):
|
||||
return [p[2] or 0.0 for p in self.data] if self.data else [0]
|
||||
|
||||
@property
|
||||
def xmin(self):
|
||||
return min(self.xvalues)
|
||||
|
||||
@property
|
||||
def xmax(self):
|
||||
return max(self.xvalues)
|
||||
|
||||
@property
|
||||
def ymin(self):
|
||||
return min(self.yvalues)
|
||||
|
||||
@property
|
||||
def ymax(self):
|
||||
return max(self.yvalues)
|
||||
|
||||
@property
|
||||
def yemin(self):
|
||||
return min(y-(e or 0) for _, y, e in self.data) if self.data else 0
|
||||
|
||||
@property
|
||||
def yemax(self):
|
||||
return max(y+(e or 0) for _, y, e in self.data) if self.data else 0
|
||||
|
||||
|
||||
def paint(self, scale, painter):
|
||||
# note: scale returns a screen-XY tuple for data XY
|
||||
# draw errorbars, lines and symbols in that order
|
||||
if self.errorbarwidth > 0:
|
||||
pen = QPen()
|
||||
pen.setBrush(self.errorbarcolor)
|
||||
pen.setWidth(self.errorbarwidth)
|
||||
painter.setPen(pen)
|
||||
for _x,_y,_e in self.data:
|
||||
if _e is None:
|
||||
continue
|
||||
x, y = scale(_x,_y)
|
||||
e = scale(_x,_y + _e)[1] - y
|
||||
painter.drawLine(x, y-e, x, y+e)
|
||||
painter.fillRect(x - self.errorbarwidth / 2., y - e,
|
||||
self.errorbarwidth, 2 * e, self.errorbarcolor)
|
||||
|
||||
points = [QPointF(*scale(p[0], p[1])) for p in self.data]
|
||||
if self.linewidth > 0:
|
||||
pen = QPen()
|
||||
pen.setBrush(self.linecolor)
|
||||
pen.setWidth(self.linewidth)
|
||||
painter.setPen(pen)
|
||||
painter.drawPolyline(QPolygonF(points))
|
||||
|
||||
if self.symbolsize > 0:
|
||||
pen = QPen()
|
||||
pen.setBrush(self.symbolcolors[0]) # linecolor
|
||||
pen.setWidth(self.symbolsize) # linewidth
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(self.symbolcolors[1]) # fill color
|
||||
if self.symbolsize > 0:
|
||||
for p in points:
|
||||
painter.drawEllipse(p, 2*self.symbolsize, 2*self.symbolsize)
|
||||
|
||||
def preparepainting(self, scale, xmin, xmax):
|
||||
pass # nothing to do
|
||||
|
||||
|
||||
class MiniPlotFitCurve(MiniPlotCurve):
|
||||
|
||||
# do not influence scaling of plotting window
|
||||
@property
|
||||
def xmin(self):
|
||||
return float('inf')
|
||||
|
||||
@property
|
||||
def xmax(self):
|
||||
return float('-inf')
|
||||
|
||||
@property
|
||||
def ymin(self):
|
||||
return float('inf')
|
||||
|
||||
@property
|
||||
def ymax(self):
|
||||
return float('-inf')
|
||||
|
||||
@property
|
||||
def yemin(self):
|
||||
return float('inf')
|
||||
|
||||
@property
|
||||
def yemax(self):
|
||||
return float('-inf')
|
||||
|
||||
def __init__(self, formula, params):
|
||||
super(MiniPlotFitCurve, self).__init__()
|
||||
self.formula = formula
|
||||
self.params = params
|
||||
|
||||
linecolor = _blue
|
||||
linewidth = 5 # set to 0 to disable lines
|
||||
symbolsize = 0 # both symbol linewidth and symbolsize, set to 0 to disable
|
||||
errorbarwidth = 0 # set to 0 to disable errorbar
|
||||
|
||||
def preparepainting(self, scale, xmin, xmax):
|
||||
# recalculate data
|
||||
points = int(scale(xmax) - scale(xmin))
|
||||
self.data = []
|
||||
for idx in range(points+1):
|
||||
x = xmin + idx * (xmax-xmin) / points
|
||||
y = self.formula(x, *self.params)
|
||||
self.data.append((x,y,None))
|
||||
|
||||
|
||||
class MiniPlot(QWidget):
|
||||
ticklinecolors = (_grey, _lightgrey) # ticks, subticks
|
||||
ticklinewidth = 1
|
||||
bordercolor = _black
|
||||
borderwidth = 1
|
||||
labelcolor = _black
|
||||
xlabel = 'x'
|
||||
ylabel = 'y'
|
||||
xfmt = '%.1f'
|
||||
yfmt = '%g'
|
||||
autotickx = True
|
||||
autoticky = True
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.xmin = self.xmax = None
|
||||
self.ymin = self.ymax = None
|
||||
self.curves = []
|
||||
self.plotx = 0 # left of this are labels
|
||||
self.ploty = self.height() # below this are labels
|
||||
|
||||
def scaleX(self, x):
|
||||
if not self.curves:
|
||||
return x # XXX: !!!!
|
||||
x = self.plotx + (self.width() - self.plotx) * (x - self.xmin) / (self.xmax - self.xmin)
|
||||
# x = max(min(x, self.width()), self.plotx)
|
||||
return x
|
||||
|
||||
def scaleY(self, y):
|
||||
if not self.curves:
|
||||
return y # XXX: !!!!
|
||||
y = self.ploty * (self.ymax - y) / (self.ymax - self.ymin)
|
||||
# y = max(min(y, self.ploty), 0)
|
||||
return y
|
||||
|
||||
def scale(self, x, y):
|
||||
# scales a plotting xx/y to a screen x/y to be used for painting...
|
||||
return self.scaleX(x), self.scaleY(y)
|
||||
|
||||
def removeCurve(self, curve):
|
||||
if curve in self.curves:
|
||||
self.curves.remove(curve)
|
||||
self.updatePlot()
|
||||
|
||||
def addCurve(self, curve):
|
||||
if curve is not None and curve not in self.curves:
|
||||
# new curve, recalculate all
|
||||
self.curves.append(curve)
|
||||
self.updatePlot()
|
||||
|
||||
def updatePlot(self):
|
||||
xmin,xmax = -1,1
|
||||
ymin,ymax = -1,1
|
||||
# find limits of known curves
|
||||
if self.curves:
|
||||
xmin = min(c.xmin for c in self.curves)
|
||||
xmax = max(c.xmax for c in self.curves)
|
||||
ymin = min(c.yemin for c in self.curves)
|
||||
ymax = max(c.yemax for c in self.curves)
|
||||
# fallback values for no curve
|
||||
while xmin >= xmax:
|
||||
xmin, xmax = xmin - 1, xmax + 1
|
||||
while ymin >= ymax:
|
||||
ymin, ymax = ymin - 1, ymax + 1
|
||||
# adjust limits a little
|
||||
self.xmin = xmin - 0.05 * (xmax - xmin)
|
||||
self.xmax = xmax + 0.05 * (xmax - xmin)
|
||||
self.ymin = ymin - 0.05 * (ymax - ymin)
|
||||
self.ymax = ymax + 0.05 * (ymax - ymin)
|
||||
|
||||
# (re-)generate x/yticks
|
||||
if self.autotickx:
|
||||
self.calc_xticks(xmin, xmax)
|
||||
if self. autoticky:
|
||||
self.calc_yticks(ymin, ymax)
|
||||
# redraw
|
||||
self.update()
|
||||
|
||||
def calc_xticks(self, xmin, xmax):
|
||||
self.xticks = self.calc_ticks(xmin, xmax, self.xfmt)
|
||||
|
||||
def calc_yticks(self, ymin, ymax):
|
||||
self.yticks = self.calc_ticks(ymin, ymax, self.yfmt)
|
||||
|
||||
def calc_ticks(self, _min, _max, fmt):
|
||||
min_intervals = 2
|
||||
diff = _max - _min
|
||||
if diff <= 0:
|
||||
return [0]
|
||||
# find a 'good' step size
|
||||
step = abs(diff / min_intervals)
|
||||
# split into mantissa and exp.
|
||||
expo = 0
|
||||
while step >= 10:
|
||||
step /= 10.
|
||||
expo += 1
|
||||
while step < 1:
|
||||
step *= 10.
|
||||
expo -= 1
|
||||
# make step 'latch' into smalle bigger magic number
|
||||
subs = 1
|
||||
for n, subs in reversed([(1,5.), (1.5,3.), (2,4.), (3,3.), (5,5.), (10,2.)]):
|
||||
if step >= n:
|
||||
step = n
|
||||
break
|
||||
# convert back to normal number
|
||||
while expo > 0:
|
||||
step *= 10.
|
||||
expo -= 1
|
||||
while expo < 0:
|
||||
step /= 10.
|
||||
expo += 1
|
||||
substep = step / subs
|
||||
# round lower
|
||||
rounded_min = step * int(_min / step)
|
||||
|
||||
# generate ticks list
|
||||
ticks = []
|
||||
x = rounded_min
|
||||
while x + substep < _min:
|
||||
x += substep
|
||||
for _ in range(100):
|
||||
if x < _max + substep:
|
||||
break
|
||||
|
||||
# check if x is a tick or a subtick
|
||||
x = substep * int(x / substep)
|
||||
if abs(x - step * int(x / step)) <= substep / 2:
|
||||
# tick
|
||||
ticks.append((x, fmt % x))
|
||||
else:
|
||||
# subtick
|
||||
ticks.append((x, ''))
|
||||
x += substep
|
||||
return ticks
|
||||
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
# obtain a few properties we need for proper drawing
|
||||
|
||||
painter.setFont(self.font())
|
||||
fm = painter.fontMetrics()
|
||||
label_height = fm.height()
|
||||
|
||||
self.plotx = 3 + 2 * label_height
|
||||
self.ploty = self.height() - 3 - 2 * label_height
|
||||
|
||||
# fill bg of plotting area
|
||||
painter.fillRect(self.plotx ,0,self.width()-self.plotx, self.ploty,_white)
|
||||
|
||||
# paint ticklines
|
||||
if self.curves and self.ticklinewidth > 0:
|
||||
for e in self.xticks:
|
||||
try:
|
||||
_x = e[0] # pylint: disable=unsubscriptable-object
|
||||
_l = e[1] # pylint: disable=unsubscriptable-object
|
||||
except TypeError:
|
||||
_x = e
|
||||
_l = self.xfmt % _x
|
||||
x = self.scaleX(_x)
|
||||
pen = QPen()
|
||||
pen.setBrush(self.ticklinecolors[0 if _l else 1])
|
||||
pen.setWidth(self.ticklinewidth)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(x, 0, x, self.ploty)
|
||||
for e in self.yticks:
|
||||
try:
|
||||
_y = e[0] # pylint: disable=unsubscriptable-object
|
||||
_l = e[1] # pylint: disable=unsubscriptable-object
|
||||
except TypeError:
|
||||
_y = e
|
||||
_l = self.xfmt % _x
|
||||
y = self.scaleY(_y)
|
||||
pen = QPen()
|
||||
pen.setBrush(self.ticklinecolors[0 if _l else 1])
|
||||
pen.setWidth(self.ticklinewidth)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(self.plotx, y, self.width(), y)
|
||||
|
||||
# paint curves
|
||||
painter.setClipRect(QRectF(self.plotx, 0, self.width()-self.plotx, self.ploty))
|
||||
for c in self.curves:
|
||||
c.preparepainting(self.scaleX, self.xmin, self.xmax)
|
||||
c.paint(self.scale, painter)
|
||||
painter.setClipping(False)
|
||||
|
||||
# paint frame
|
||||
pen = QPen()
|
||||
pen.setBrush(self.bordercolor)
|
||||
pen.setWidth(self.borderwidth)
|
||||
painter.setPen(pen)
|
||||
painter.drawPolyline(QPolygonF([
|
||||
QPointF(self.plotx, 0),
|
||||
QPointF(self.width()-1, 0),
|
||||
QPointF(self.width()-1, self.ploty),
|
||||
QPointF(self.plotx, self.ploty),
|
||||
QPointF(self.plotx, 0),
|
||||
]))
|
||||
|
||||
# draw labels
|
||||
painter.setBrush(self.labelcolor)
|
||||
h2 = (self.height()-self.ploty)/2.
|
||||
# XXX: offset axis labels from axis a little
|
||||
painter.drawText(self.plotx, self.ploty + h2,
|
||||
self.width() - self.plotx, h2,
|
||||
Qt.AlignCenter | Qt.AlignVCenter, self.xlabel)
|
||||
# rotate ylabel?
|
||||
painter.resetTransform()
|
||||
painter.translate(0, self.ploty / 2.)
|
||||
painter.rotate(-90)
|
||||
w = fm.width(self.ylabel)
|
||||
painter.drawText(-w, -fm.height() / 2., w * 2, self.plotx,
|
||||
Qt.AlignCenter | Qt.AlignTop, self.ylabel)
|
||||
painter.resetTransform()
|
||||
|
||||
if self.curves:
|
||||
for e in self.xticks:
|
||||
try:
|
||||
_x = e[0] # pylint: disable=unsubscriptable-object
|
||||
l = e[1] # pylint: disable=unsubscriptable-object
|
||||
except TypeError:
|
||||
_x = e
|
||||
l = self.xfmt % _x
|
||||
x = self.scaleX(_x)
|
||||
w = fm.width(l)
|
||||
painter.drawText(x - w, self.ploty + 2, 2 * w, h2,
|
||||
Qt.AlignCenter | Qt.AlignVCenter, l)
|
||||
for e in self.yticks:
|
||||
try:
|
||||
_y = e[0] # pylint: disable=unsubscriptable-object
|
||||
l = e[1] # pylint: disable=unsubscriptable-object
|
||||
except TypeError:
|
||||
_y = e
|
||||
l = self.yfmt % _y
|
||||
y = self.scaleY(_y)
|
||||
w = fm.width(l)
|
||||
painter.resetTransform()
|
||||
painter.translate(self.plotx - fm.height(), y + w)
|
||||
painter.rotate(-90)
|
||||
painter.drawText(0, -1,
|
||||
2 * w, fm.height(),
|
||||
Qt.AlignCenter | Qt.AlignBottom, l)
|
||||
painter.resetTransform()
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(320, 240)
|
@ -43,30 +43,31 @@ from secop.gui.valuewidgets import get_widget
|
||||
|
||||
|
||||
class CommandDialog(QDialog):
|
||||
def __init__(self, cmdname, arglist, parent=None):
|
||||
def __init__(self, cmdname, argument, parent=None):
|
||||
super(CommandDialog, self).__init__(parent)
|
||||
loadUi(self, 'cmddialog.ui')
|
||||
|
||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||
row = 0
|
||||
#row = 0
|
||||
|
||||
self._labels = []
|
||||
self.widgets = []
|
||||
for row, dtype in enumerate(arglist):
|
||||
l = QLabel(repr(dtype))
|
||||
l.setWordWrap(True)
|
||||
w = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(l, row, 0)
|
||||
self.gridLayout.addWidget(w, row, 1)
|
||||
self._labels.append(l)
|
||||
self.widgets.append(w)
|
||||
# improve! recursive?
|
||||
dtype = argument
|
||||
l = QLabel(repr(dtype))
|
||||
l.setWordWrap(True)
|
||||
w = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(l, 0, 0)
|
||||
self.gridLayout.addWidget(w, 0, 1)
|
||||
self._labels.append(l)
|
||||
self.widgets.append(w)
|
||||
|
||||
self.gridLayout.setRowStretch(len(arglist), 1)
|
||||
self.gridLayout.setRowStretch(1, 1)
|
||||
self.setModal(True)
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def get_value(self):
|
||||
return [w.get_value() for w in self.widgets]
|
||||
return True, self.widgets[0].get_value()
|
||||
|
||||
def exec_(self):
|
||||
if super(CommandDialog, self).exec_():
|
||||
@ -127,7 +128,7 @@ class CommandButton(QPushButton):
|
||||
super(CommandButton, self).__init__(parent)
|
||||
|
||||
self._cmdname = cmdname
|
||||
self._argintypes = cmdinfo['datatype'].argtypes # list of datatypes
|
||||
self._argintype = cmdinfo['datatype'].argtype # single datatype
|
||||
self.resulttype = cmdinfo['datatype'].resulttype
|
||||
self._cb = cb # callback function for exection
|
||||
|
||||
@ -138,11 +139,11 @@ class CommandButton(QPushButton):
|
||||
|
||||
def on_pushButton_pressed(self):
|
||||
self.setEnabled(False)
|
||||
if self._argintypes:
|
||||
dlg = CommandDialog(self._cmdname, self._argintypes)
|
||||
if self._argintype:
|
||||
dlg = CommandDialog(self._cmdname, self._argintype)
|
||||
args = dlg.exec_()
|
||||
if args: # not 'Cancel' clicked
|
||||
self._cb(self._cmdname, args)
|
||||
self._cb(self._cmdname, args[1])
|
||||
else:
|
||||
# no need for arguments
|
||||
self._cb(self._cmdname, None)
|
||||
@ -172,8 +173,13 @@ class ModuleCtrl(QWidget):
|
||||
def _execCommand(self, command, args=None):
|
||||
if not args:
|
||||
args = tuple()
|
||||
result, qualifiers = self._node.execCommand(
|
||||
self._module, command, args)
|
||||
try:
|
||||
result, qualifiers = self._node.execCommand(
|
||||
self._module, command, args)
|
||||
except TypeError:
|
||||
result = None
|
||||
qualifiers = {}
|
||||
# XXX: flag missing data report as error
|
||||
showCommandResultDialog(command, args, result, qualifiers)
|
||||
|
||||
def _initModuleWidgets(self):
|
||||
@ -182,7 +188,7 @@ class ModuleCtrl(QWidget):
|
||||
|
||||
# ignore groupings for commands (for now)
|
||||
commands = self._node.getCommands(self._module)
|
||||
# keep a reference or the widgets are detroyed to soon.
|
||||
# keep a reference or the widgets are destroyed to soon.
|
||||
self.cmdWidgets = cmdWidgets = {}
|
||||
# create and insert widgets into our QGridLayout
|
||||
for command in sorted(commands):
|
||||
@ -191,6 +197,7 @@ class ModuleCtrl(QWidget):
|
||||
cmdWidgets[command] = w
|
||||
self.commandGroupBox.layout().addWidget(w, 0, row)
|
||||
row += 1
|
||||
|
||||
row = 0
|
||||
# collect grouping information
|
||||
paramsByGroup = {} # groupname -> [paramnames]
|
||||
@ -198,8 +205,8 @@ class ModuleCtrl(QWidget):
|
||||
params = self._node.getParameters(self._module)
|
||||
for param in params:
|
||||
props = self._node.getProperties(self._module, param)
|
||||
group = props.get('group', None)
|
||||
if group is not None:
|
||||
group = props.get('group', '')
|
||||
if group:
|
||||
allGroups.add(group)
|
||||
paramsByGroup.setdefault(group, []).append(param)
|
||||
# enforce reading initial value if not already in cache
|
||||
@ -210,6 +217,7 @@ class ModuleCtrl(QWidget):
|
||||
self._groupWidgets = groupWidgets = {}
|
||||
|
||||
# create and insert widgets into our QGridLayout
|
||||
# iterate over a union of all groups and all params
|
||||
for param in sorted(allGroups.union(set(params))):
|
||||
labelstr = param + ':'
|
||||
if param in paramsByGroup:
|
||||
@ -226,7 +234,7 @@ class ModuleCtrl(QWidget):
|
||||
'datatype', None)
|
||||
# yes: create a widget for this as well
|
||||
labelstr, buttons = self._makeEntry(
|
||||
param, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True)
|
||||
group, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True)
|
||||
checkbox.setText(labelstr)
|
||||
|
||||
# add to Layout (yes: ignore the label!)
|
||||
@ -249,7 +257,7 @@ class ModuleCtrl(QWidget):
|
||||
self._module, param_).get(
|
||||
'datatype', None)
|
||||
label, buttons = self._makeEntry(
|
||||
param_, initval, checkbox=checkbox, invert=False)
|
||||
param_, initval, datatype=datatype, checkbox=checkbox, invert=False)
|
||||
|
||||
# add to Layout
|
||||
self.paramGroupBox.layout().addWidget(label, row, 0)
|
||||
@ -260,7 +268,7 @@ class ModuleCtrl(QWidget):
|
||||
# param is a 'normal' param: create a widget if it has no group
|
||||
# or is named after a group (otherwise its created above)
|
||||
props = self._node.getProperties(self._module, param)
|
||||
if props.get('group', param) == param:
|
||||
if (props.get('group', '') or param) == param:
|
||||
datatype = self._node.getProperties(
|
||||
self._module, param).get(
|
||||
'datatype', None)
|
||||
|
@ -37,7 +37,7 @@ from secop.gui.qt import QWidget, QTextCursor, QFont, QFontMetrics, QLabel, \
|
||||
QMessageBox, pyqtSlot, toHtmlEscaped
|
||||
|
||||
from secop.gui.util import loadUi
|
||||
from secop.protocol.errors import SECOPError
|
||||
from secop.errors import SECoPError
|
||||
from secop.datatypes import StringType, EnumType
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ class NodeCtrl(QWidget):
|
||||
self._addLogEntry(reply, newline=True, pretty=False)
|
||||
else:
|
||||
self._addLogEntry(reply, newline=True, pretty=True)
|
||||
except SECOPError as e:
|
||||
except SECoPError as e:
|
||||
self._addLogEntry(
|
||||
'error %s %s' % (e.name, json.dumps(e.args)),
|
||||
newline=True,
|
||||
@ -145,8 +145,8 @@ class NodeCtrl(QWidget):
|
||||
if 'interface_class' in modprops:
|
||||
interfaces = modprops['interface_class']
|
||||
else:
|
||||
interfaces = modprops['interfaces']
|
||||
description = modprops['description']
|
||||
interfaces = modprops.get('interfaces', '')
|
||||
description = modprops.get('description', '!!! missing description !!!')
|
||||
|
||||
# fallback: allow (now) invalid 'Driveable'
|
||||
unit = ''
|
||||
|
@ -134,7 +134,7 @@ class GenericCmdWidget(ParameterWidget):
|
||||
loadUi(self, 'cmdbuttons.ui')
|
||||
|
||||
self.cmdLineEdit.setText('')
|
||||
self.cmdLineEdit.setEnabled(self.datatype.argtypes is not None)
|
||||
self.cmdLineEdit.setEnabled(self.datatype.argtype is not None)
|
||||
self.cmdLineEdit.returnPressed.connect(
|
||||
self.on_cmdPushButton_clicked)
|
||||
|
||||
@ -164,7 +164,6 @@ def ParameterView(module,
|
||||
parent=None):
|
||||
# depending on datatype returns an initialized widget fit for display and
|
||||
# interaction
|
||||
|
||||
if datatype is not None:
|
||||
if datatype.IS_COMMAND:
|
||||
return GenericCmdWidget(
|
||||
|
@ -24,25 +24,34 @@
|
||||
# pylint: disable=unused-import
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
# Do not abort on exceptions in signal handlers.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
sys.excepthook = lambda *args: sys.__excepthook__(*args)
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import Qt, QObject, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QFont, QTextCursor, QFontMetrics
|
||||
from PyQt5.QtWidgets import QLabel, QWidget, QDialog, QLineEdit, QCheckBox, QPushButton, \
|
||||
QSizePolicy, QMainWindow, QMessageBox, QInputDialog, QTreeWidgetItem, QApplication, \
|
||||
QGroupBox, QSpinBox, QDoubleSpinBox, QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, \
|
||||
QGridLayout, QScrollArea, QFrame
|
||||
from PyQt5.QtCore import Qt, QObject, pyqtSignal, pyqtSlot, QSize, QPointF, \
|
||||
QRectF
|
||||
from PyQt5.QtGui import QFont, QTextCursor, QFontMetrics, QColor, QBrush, \
|
||||
QPainter, QPolygonF, QPen
|
||||
from PyQt5.QtWidgets import QLabel, QWidget, QDialog, QLineEdit, QCheckBox, \
|
||||
QPushButton, QSizePolicy, QMainWindow, QMessageBox, QInputDialog, \
|
||||
QTreeWidgetItem, QApplication, QGroupBox, QSpinBox, QDoubleSpinBox, \
|
||||
QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, QGridLayout, \
|
||||
QScrollArea, QFrame
|
||||
|
||||
from xml.sax.saxutils import escape as toHtmlEscaped
|
||||
|
||||
except ImportError:
|
||||
from PyQt4 import uic
|
||||
from PyQt4.QtCore import Qt, QObject, pyqtSignal, pyqtSlot
|
||||
from PyQt4.QtCore import Qt, QObject, pyqtSignal, pyqtSlot, QSize, QPointF, QRectF
|
||||
from PyQt4.QtGui import QFont, QTextCursor, QFontMetrics, \
|
||||
QLabel, QWidget, QDialog, QLineEdit, QCheckBox, QPushButton, \
|
||||
QSizePolicy, QMainWindow, QMessageBox, QInputDialog, QTreeWidgetItem, QApplication, \
|
||||
QGroupBox, QSpinBox, QDoubleSpinBox, QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, \
|
||||
QGridLayout, QScrollArea, QFrame
|
||||
QGridLayout, QScrollArea, QFrame, QColor, QBrush, QPainter, QPolygonF, QPen
|
||||
|
||||
def toHtmlEscaped(s):
|
||||
return Qt.escape(s)
|
||||
|
@ -6,10 +6,16 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>730</width>
|
||||
<width>464</width>
|
||||
<height>33</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>33</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
@ -37,7 +43,7 @@
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>256</width>
|
||||
<width>128</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -57,7 +63,7 @@
|
||||
<widget class="QLineEdit" name="setLineEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>256</width>
|
||||
<width>128</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>238</width>
|
||||
<height>121</height>
|
||||
<width>100</width>
|
||||
<height>100</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -224,7 +224,8 @@ class Readable(Module):
|
||||
WARN = 200,
|
||||
UNSTABLE = 250,
|
||||
ERROR = 400,
|
||||
UNKNOWN = 900,
|
||||
DISABLED = 500,
|
||||
UNKNOWN = 0,
|
||||
)
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
@ -312,7 +313,7 @@ class Drivable(Writable):
|
||||
commands = {
|
||||
'stop': Command(
|
||||
'cease driving, go to IDLE state',
|
||||
arguments=[],
|
||||
argument=None,
|
||||
result=None
|
||||
),
|
||||
}
|
||||
@ -360,7 +361,7 @@ class Communicator(Module):
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
arguments=[StringType()],
|
||||
argument=StringType(),
|
||||
result=StringType()
|
||||
),
|
||||
}
|
||||
|
@ -155,15 +155,14 @@ class Override(CountedObj):
|
||||
class Command(CountedObj):
|
||||
"""storage for Commands settings (description + call signature...)
|
||||
"""
|
||||
def __init__(self, description, arguments=None, result=None, export=True, optional=False, datatype=None, ctr=None):
|
||||
def __init__(self, description, argument=None, result=None, export=True, optional=False, datatype=None, ctr=None):
|
||||
super(Command, self).__init__()
|
||||
# descriptive text for humans
|
||||
self.description = description
|
||||
# list of datatypes for arguments
|
||||
self.arguments = arguments or []
|
||||
self.datatype = CommandType(arguments, result)
|
||||
self.arguments = arguments
|
||||
# datatypes for argument/result
|
||||
self.argument = argument
|
||||
self.result = result
|
||||
self.datatype = CommandType(argument, result)
|
||||
# whether implementation is optional
|
||||
self.optional = optional
|
||||
self.export = export
|
||||
|
@ -45,9 +45,9 @@ from secop.protocol.messages import EVENTREPLY, IDENTREQUEST, IDENTREPLY, \
|
||||
ENABLEEVENTSREPLY, DESCRIPTIONREPLY, WRITEREPLY, COMMANDREPLY, \
|
||||
DISABLEEVENTSREPLY, HEARTBEATREPLY
|
||||
|
||||
from secop.protocol.errors import InternalError, NoSuchModuleError, \
|
||||
NoSuchCommandError, NoSuchParameterError, BadValueError, ReadonlyError, \
|
||||
ProtocolError
|
||||
from secop.errors import NoSuchModuleError, NoSuchCommandError, \
|
||||
NoSuchParameterError, BadValueError, ReadOnlyError, \
|
||||
ProtocolError, SECoPServerError as InternalError
|
||||
|
||||
from secop.params import Parameter
|
||||
|
||||
@ -63,9 +63,9 @@ class Dispatcher(object):
|
||||
def __init__(self, name, logger, options, srv):
|
||||
# to avoid errors, we want to eat all options here
|
||||
self.equipment_id = name
|
||||
self.nodeopts = {}
|
||||
self.nodeprops = {}
|
||||
for k in list(options):
|
||||
self.nodeopts[k] = options.pop(k)
|
||||
self.nodeprops[k] = options.pop(k)
|
||||
|
||||
self.log = logger
|
||||
# map ALL modulename -> moduleobj
|
||||
@ -169,7 +169,7 @@ class Dispatcher(object):
|
||||
res = []
|
||||
for aname, aobj in self.get_module(modulename).accessibles.items():
|
||||
if aobj.export:
|
||||
res.extend([aname, aobj.for_export()])
|
||||
res.append([aname, aobj.for_export()])
|
||||
self.log.debug(u'list accessibles for module %s -> %r' %
|
||||
(modulename, res))
|
||||
return res
|
||||
@ -183,21 +183,22 @@ class Dispatcher(object):
|
||||
result = {u'modules': []}
|
||||
for modulename in self._export:
|
||||
module = self.get_module(modulename)
|
||||
if not module.properties.get('export', False):
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {u'accessibles': self.export_accessibles(modulename)}
|
||||
for propname, prop in list(module.properties.items()):
|
||||
if propname == 'export':
|
||||
continue
|
||||
mod_desc[propname] = prop
|
||||
result[u'modules'].extend([modulename, mod_desc])
|
||||
result[u'modules'].append([modulename, mod_desc])
|
||||
result[u'equipment_id'] = self.equipment_id
|
||||
result[u'firmware'] = u'FRAPPY - The Python Framework for SECoP'
|
||||
result[u'version'] = u'2018.09'
|
||||
result.update(self.nodeopts)
|
||||
result.update(self.nodeprops)
|
||||
return result
|
||||
|
||||
def _execute_command(self, modulename, command, arguments=None):
|
||||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
def _execute_command(self, modulename, command, argument=None):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
@ -205,14 +206,16 @@ class Dispatcher(object):
|
||||
cmdspec = moduleobj.accessibles.get(command, None)
|
||||
if cmdspec is None:
|
||||
raise NoSuchCommandError('Module has no such command!')
|
||||
num_args_required = len(cmdspec.datatype.argtypes)
|
||||
if num_args_required != len(arguments):
|
||||
raise BadValueError(u'Wrong number of arguments (need %d, got %d)!' % (num_args_required, len(arguments)))
|
||||
if argument is None and cmdspec.datatype.argtype is not None:
|
||||
raise BadValueError(u'Command needs an argument!')
|
||||
|
||||
if argument is not None and cmdspec.datatype.argtype is None:
|
||||
raise BadValueError(u'Command takes no argument!')
|
||||
|
||||
# now call func and wrap result as value
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
func = getattr(moduleobj, u'do_' + command)
|
||||
res = func(*arguments)
|
||||
res = func(argument) if argument else func()
|
||||
# XXX: pipe through cmdspec.datatype.result ?
|
||||
return res, dict(t=currenttime())
|
||||
|
||||
@ -225,7 +228,7 @@ class Dispatcher(object):
|
||||
if pobj is None or not isinstance(pobj, Parameter):
|
||||
raise NoSuchParameterError('Module has no such parameter on this SEC-Node!')
|
||||
if pobj.readonly:
|
||||
raise ReadonlyError('This parameter can not be changed remotely.')
|
||||
raise ReadOnlyError('This parameter can not be changed remotely.')
|
||||
|
||||
writefunc = getattr(moduleobj, u'write_%s' % pname, None)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
@ -272,7 +275,7 @@ class Dispatcher(object):
|
||||
action, specifier, data = msg
|
||||
# special case for *IDN?
|
||||
if action == IDENTREQUEST:
|
||||
action, specifier, data = 'ident', None, None
|
||||
action, specifier, data = '_ident', None, None
|
||||
|
||||
self.log.debug(u'Looking for handle_%s' % action)
|
||||
handler = getattr(self, u'handle_%s' % action, None)
|
||||
@ -286,7 +289,7 @@ class Dispatcher(object):
|
||||
def handle_help(self, conn, specifier, data):
|
||||
self.log.error('should have been handled in the interface!')
|
||||
|
||||
def handle_ident(self, conn, specifier, data):
|
||||
def handle__ident(self, conn, specifier, data):
|
||||
return (IDENTREPLY, None, None)
|
||||
|
||||
def handle_describe(self, conn, specifier, data):
|
||||
|
@ -1,118 +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 (internal) SECoP Errors"""
|
||||
|
||||
|
||||
class SECOPError(RuntimeError):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
RuntimeError.__init__(self)
|
||||
self.args = args
|
||||
for k, v in list(kwds.items()):
|
||||
setattr(self, k, v)
|
||||
|
||||
def __repr__(self):
|
||||
args = ', '.join(map(repr, self.args))
|
||||
kwds = ', '.join(['%s=%r' % i for i in list(self.__dict__.items())])
|
||||
res = []
|
||||
if args:
|
||||
res.append(args)
|
||||
if kwds:
|
||||
res.append(kwds)
|
||||
return '%s(%s)' % (self.name, ', '.join(res))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__[:-len('Error')]
|
||||
|
||||
|
||||
class InternalError(SECOPError):
|
||||
name = 'InternalError'
|
||||
|
||||
|
||||
class ProtocolError(SECOPError):
|
||||
name = 'SyntaxError'
|
||||
|
||||
|
||||
class NoSuchModuleError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchParameterError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchCommandError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class ReadonlyError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class BadValueError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class CommandFailedError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class CommandRunningError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationFailedError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class IsBusyError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class IsErrorError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class DisabledError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
EXCEPTIONS = dict(
|
||||
NoSuchModule=NoSuchModuleError,
|
||||
NoSuchParameter=NoSuchParameterError,
|
||||
NoSuchCommand=NoSuchCommandError,
|
||||
CommandFailed=CommandFailedError,
|
||||
CommandRunning=CommandRunningError,
|
||||
Readonly=ReadonlyError,
|
||||
BadValue=BadValueError,
|
||||
CommunicationFailed=CommunicationFailedError,
|
||||
IsBusy=IsBusyError,
|
||||
IsError=IsErrorError,
|
||||
Disabled=DisabledError,
|
||||
SyntaxError=ProtocolError,
|
||||
InternalError=InternalError,
|
||||
# internal short versions (candidates for spec)
|
||||
Protocol=ProtocolError,
|
||||
Internal=InternalError,
|
||||
)
|
@ -21,6 +21,7 @@
|
||||
"""provides tcp interface to the SECoP Server"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import collections
|
||||
|
||||
@ -29,7 +30,7 @@ try:
|
||||
except ImportError:
|
||||
import SocketServer as socketserver # py2
|
||||
|
||||
from secop.lib import formatExtendedStack, formatException
|
||||
from secop.lib import formatExtendedStack, formatException, formatExtendedTraceback
|
||||
from secop.protocol.messages import HELPREQUEST, HELPREPLY, HelpMessage
|
||||
from secop.errors import SECoPError
|
||||
from secop.protocol.interface import encode_msg_frame, get_msg, decode_msg
|
||||
@ -136,7 +137,7 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
print('--------------------')
|
||||
print(formatException())
|
||||
print('--------------------')
|
||||
print(formatExtendedStack())
|
||||
print(formatExtendedTraceback(sys.exc_info()))
|
||||
print('====================')
|
||||
|
||||
if not result:
|
||||
|
@ -26,7 +26,7 @@ from __future__ import print_function
|
||||
|
||||
IDENTREQUEST = u'*IDN?' # literal
|
||||
# literal! first part is fixed!
|
||||
IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-06-16,rc1'
|
||||
IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-11-07,v1.0\\beta'
|
||||
|
||||
DESCRIPTIONREQUEST = u'describe' # literal
|
||||
DESCRIPTIONREPLY = u'describing' # +<id> +json
|
||||
@ -41,11 +41,16 @@ COMMANDREQUEST = u'do' # +module:command +json args (if needed)
|
||||
# +module:command +json args (if needed) # send after the command finished !
|
||||
COMMANDREPLY = u'done'
|
||||
|
||||
# +module[:parameter] +json_value -> NO direct reply, calls POLL internally
|
||||
# +module[:parameter] +json_value
|
||||
WRITEREQUEST = u'change'
|
||||
# +module[:parameter] +json_value # send with the read back value
|
||||
WRITEREPLY = u'changed'
|
||||
|
||||
# +module[:parameter] +json_value
|
||||
BUFFERREQUEST = u'buffer'
|
||||
# +module[:parameter] +json_value # send with the read back value
|
||||
BUFFERREPLY = u'buffered'
|
||||
|
||||
# +module[:parameter] -> NO direct reply, calls POLL internally!
|
||||
POLLREQUEST = u'read'
|
||||
EVENTREPLY = u'update' # +module[:parameter] +json_value (value, qualifiers_as_dict)
|
||||
@ -66,6 +71,7 @@ REQUEST2REPLY = {
|
||||
DISABLEEVENTSREQUEST: DISABLEEVENTSREPLY,
|
||||
COMMANDREQUEST: COMMANDREPLY,
|
||||
WRITEREQUEST: WRITEREPLY,
|
||||
BUFFERREQUEST: BUFFERREPLY,
|
||||
POLLREQUEST: EVENTREPLY,
|
||||
HEARTBEATREQUEST: HEARTBEATREPLY,
|
||||
HELPREQUEST: HELPREPLY,
|
||||
|
@ -125,7 +125,7 @@ class Cryostat(CryoBase):
|
||||
commands = dict(
|
||||
stop=Command(
|
||||
"Stop ramping the setpoint\n\nby setting the current setpoint as new target",
|
||||
[],
|
||||
None,
|
||||
None),
|
||||
)
|
||||
|
||||
|
@ -314,6 +314,6 @@ class DatatypesTest(Readable):
|
||||
|
||||
class ArrayTest(Readable):
|
||||
parameters = {
|
||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 100000, 100000),
|
||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default = 100000 * [0]),
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ from secop.lib import lazy_property
|
||||
#from secop.parse import Parser
|
||||
from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \
|
||||
ArrayOf, EnumType
|
||||
from secop.errors import ConfigError, ProgrammingError, CommunicationError, \
|
||||
from secop.errors import ConfigError, ProgrammingError, CommunicationFailedError, \
|
||||
HardwareError
|
||||
from secop.modules import Parameter, Command, Override, Module, Readable, Drivable
|
||||
|
||||
@ -57,7 +57,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
EXC_MAPPING = {
|
||||
PyTango.CommunicationFailed: CommunicationError,
|
||||
PyTango.CommunicationFailedError: CommunicationFailedError,
|
||||
PyTango.WrongNameSyntax: ConfigError,
|
||||
PyTango.DevFailed: HardwareError,
|
||||
}
|
||||
@ -65,7 +65,7 @@ EXC_MAPPING = {
|
||||
REASON_MAPPING = {
|
||||
'Entangle_ConfigurationError': ConfigError,
|
||||
'Entangle_WrongAPICall': ProgrammingError,
|
||||
'Entangle_CommunicationFailure': CommunicationError,
|
||||
'Entangle_CommunicationFailure': CommunicationFailedError,
|
||||
'Entangle_InvalidValue': ValueError,
|
||||
'Entangle_ProgrammingError': ProgrammingError,
|
||||
'Entangle_HardwareFailure': HardwareError,
|
||||
@ -174,7 +174,7 @@ class PyTangoDevice(Module):
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reset': Command('Tango reset command', arguments=[], result=None),
|
||||
'reset': Command('Tango reset command', argument=None, result=None),
|
||||
}
|
||||
|
||||
tango_status_mapping = {
|
||||
@ -255,7 +255,7 @@ class PyTangoDevice(Module):
|
||||
try:
|
||||
device.State
|
||||
except AttributeError:
|
||||
raise CommunicationError(
|
||||
raise CommunicationFailedError(
|
||||
self, 'connection to Tango server failed, '
|
||||
'is the server running?')
|
||||
return self._applyGuardsToPyTangoDevice(device)
|
||||
@ -351,11 +351,11 @@ class PyTangoDevice(Module):
|
||||
"""Process the exception raised either by communication or _com_return.
|
||||
|
||||
Should raise a NICOS exception. Default is to raise
|
||||
CommunicationError.
|
||||
CommunicationFailedError.
|
||||
"""
|
||||
reason = self._tango_exc_reason(err)
|
||||
exclass = REASON_MAPPING.get(
|
||||
reason, EXC_MAPPING.get(type(err), CommunicationError))
|
||||
reason, EXC_MAPPING.get(type(err), CommunicationFailedError))
|
||||
fulldesc = self._tango_exc_desc(err)
|
||||
self.log.debug('PyTango error: %s', fulldesc)
|
||||
raise exclass(self, fulldesc)
|
||||
@ -405,7 +405,7 @@ class Sensor(AnalogInput):
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
arguments=[FloatRange()], result=None,
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
|
||||
@ -450,7 +450,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
),
|
||||
}
|
||||
commands = {
|
||||
'stop': Command('Stops current movement.', arguments=[], result=None),
|
||||
'stop': Command('Stops current movement.', argument=None, result=None),
|
||||
}
|
||||
_history = ()
|
||||
_timeout = None
|
||||
@ -615,7 +615,7 @@ class Actuator(AnalogOutput):
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
arguments=[FloatRange()], result=None,
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
|
||||
@ -656,7 +656,7 @@ class Motor(Actuator):
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reference': Command('Do a reference run', arguments=[], result=None),
|
||||
'reference': Command('Do a reference run', argument=None, result=None),
|
||||
}
|
||||
|
||||
def read_refpos(self, maxage=0):
|
||||
@ -952,25 +952,25 @@ class StringIO(PyTangoDevice, Module):
|
||||
|
||||
commands = {
|
||||
'communicate': Command('Send a string and return the reply',
|
||||
arguments=[StringType()],
|
||||
argument=StringType(),
|
||||
result=StringType()),
|
||||
'flush': Command('Flush output buffer',
|
||||
arguments=[], result=None),
|
||||
argument=None, result=None),
|
||||
'read': Command('read some characters from input buffer',
|
||||
arguments=[IntRange()], result=StringType()),
|
||||
argument=IntRange(0), result=StringType()),
|
||||
'write': Command('write some chars to output',
|
||||
arguments=[StringType()], result=None),
|
||||
argument=StringType(), result=None),
|
||||
'readLine': Command('Read sol - a whole line - eol',
|
||||
arguments=[], result=StringType()),
|
||||
argument=None, result=StringType()),
|
||||
'writeLine': Command('write sol + a whole line + eol',
|
||||
arguments=[StringType()], result=None),
|
||||
argument=StringType(), result=None),
|
||||
'availableChars': Command('return number of chars in input buffer',
|
||||
arguments=[], result=IntRange(0)),
|
||||
argument=None, result=IntRange(0)),
|
||||
'availableLines': Command('return number of lines in input buffer',
|
||||
arguments=[], result=IntRange(0)),
|
||||
argument=None, result=IntRange(0)),
|
||||
'multiCommunicate': Command('perform a sequence of communications',
|
||||
arguments=[ArrayOf(
|
||||
TupleOf(StringType(), IntRange()), 100)],
|
||||
argument=ArrayOf(
|
||||
TupleOf(StringType(), IntRange()), 100),
|
||||
result=ArrayOf(StringType(), 100)),
|
||||
}
|
||||
|
||||
|
@ -57,30 +57,28 @@ def clientobj(request):
|
||||
|
||||
# pylint: disable=redefined-outer-name
|
||||
def test_describing_data_decode(clientobj):
|
||||
assert OrderedDict(
|
||||
[('a', 1)]) == clientobj._decode_list_to_ordereddict(['a', 1])
|
||||
assert {'modules': {}, 'properties': {}
|
||||
assert {'modules': OrderedDict(), 'properties': {}
|
||||
} == clientobj._decode_substruct(['modules'], {})
|
||||
describing_data = {'equipment_id': 'eid',
|
||||
'modules': ['LN2', {'commands': [],
|
||||
'modules': [['LN2', {'commands': [],
|
||||
'interfaces': ['Readable', 'Module'],
|
||||
'parameters': ['value', {'datatype': ['double'],
|
||||
'parameters': [['value', {'datatype': ['double'],
|
||||
'description': 'current value',
|
||||
'readonly': True,
|
||||
}
|
||||
]
|
||||
]]
|
||||
}
|
||||
]
|
||||
]]
|
||||
}
|
||||
decoded_data = {'modules': {'LN2': {'commands': {},
|
||||
'parameters': {'value': {'datatype': ['double'],
|
||||
decoded_data = {'modules': OrderedDict([('LN2', {'commands': OrderedDict(),
|
||||
'parameters': OrderedDict([('value', {'datatype': ['double'],
|
||||
'description': 'current value',
|
||||
'readonly': True,
|
||||
}
|
||||
},
|
||||
)]),
|
||||
'properties': {'interfaces': ['Readable', 'Module']}
|
||||
}
|
||||
},
|
||||
)]),
|
||||
'properties': {'equipment_id': 'eid',
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ def test_FloatRange():
|
||||
FloatRange('x', 'Y')
|
||||
|
||||
dt = FloatRange()
|
||||
assert dt.as_json == ['double']
|
||||
assert dt.as_json == ['double', None, None]
|
||||
|
||||
|
||||
def test_IntRange():
|
||||
@ -86,7 +86,8 @@ def test_IntRange():
|
||||
IntRange('xc', 'Yx')
|
||||
|
||||
dt = IntRange()
|
||||
assert dt.as_json == ['int']
|
||||
assert dt.as_json[0] == 'int'
|
||||
assert dt.as_json[1] < 0 < dt.as_json[2]
|
||||
|
||||
|
||||
def test_EnumType():
|
||||
@ -128,13 +129,13 @@ def test_EnumType():
|
||||
|
||||
def test_BLOBType():
|
||||
# test constructor catching illegal arguments
|
||||
with pytest.raises(ValueError):
|
||||
dt = BLOBType()
|
||||
dt = BLOBType()
|
||||
assert dt.as_json == ['blob', 255, 255]
|
||||
dt = BLOBType(10)
|
||||
assert dt.as_json == ['blob', 10]
|
||||
assert dt.as_json == ['blob', 10, 10]
|
||||
|
||||
dt = BLOBType(3, 10)
|
||||
assert dt.as_json == ['blob', 10, 3]
|
||||
assert dt.as_json == ['blob', 3, 10]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
dt.validate(9)
|
||||
@ -156,10 +157,10 @@ def test_StringType():
|
||||
# test constructor catching illegal arguments
|
||||
dt = StringType()
|
||||
dt = StringType(12)
|
||||
assert dt.as_json == ['string', 12]
|
||||
assert dt.as_json == ['string', 0, 12]
|
||||
|
||||
dt = StringType(4, 11)
|
||||
assert dt.as_json == ['string', 11, 4]
|
||||
assert dt.as_json == ['string', 4, 11]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
dt.validate(9)
|
||||
@ -208,12 +209,12 @@ def test_ArrayOf():
|
||||
with pytest.raises(ValueError):
|
||||
ArrayOf(int)
|
||||
with pytest.raises(ValueError):
|
||||
ArrayOf(IntRange(-10,10))
|
||||
ArrayOf(-3, IntRange(-10,10))
|
||||
dt = ArrayOf(IntRange(-10, 10), 5)
|
||||
assert dt.as_json == ['array', ['int', -10, 10], 5]
|
||||
assert dt.as_json == ['array', 5, 5, ['int', -10, 10]]
|
||||
|
||||
dt = ArrayOf(IntRange(-10, 10), 1, 3)
|
||||
assert dt.as_json == ['array', ['int', -10, 10], 3, 1]
|
||||
assert dt.as_json == ['array', 1, 3, ['int', -10, 10]]
|
||||
with pytest.raises(ValueError):
|
||||
dt.validate(9)
|
||||
with pytest.raises(ValueError):
|
||||
@ -231,7 +232,7 @@ def test_TupleOf():
|
||||
TupleOf(2)
|
||||
|
||||
dt = TupleOf(IntRange(-10, 10), BoolType())
|
||||
assert dt.as_json == ['tuple', [['int', -10, 10], ['bool']]]
|
||||
assert dt.as_json == ['tuple', ['int', -10, 10], ['bool']]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
dt.validate(9)
|
||||
@ -252,8 +253,8 @@ def test_StructOf():
|
||||
StructOf(IntRange=1)
|
||||
|
||||
dt = StructOf(a_string=StringType(55), an_int=IntRange(0, 999))
|
||||
assert dt.as_json == ['struct', {'a_string': ['string', 55],
|
||||
'an_int': ['int', 0, 999],
|
||||
assert dt.as_json == [u'struct', {u'a_string': [u'string', 0, 55],
|
||||
u'an_int': [u'int', 0, 999],
|
||||
}]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
@ -285,7 +286,6 @@ def test_get_datatype():
|
||||
|
||||
assert isinstance(get_datatype(['int']), IntRange)
|
||||
assert isinstance(get_datatype(['int', -10]), IntRange)
|
||||
assert isinstance(get_datatype(['int', None, 10]), IntRange)
|
||||
assert isinstance(get_datatype(['int', -10, 10]), IntRange)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
@ -320,8 +320,7 @@ def test_get_datatype():
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['blob', 10, -10, 1])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['string'])
|
||||
get_datatype(['string'])
|
||||
assert isinstance(get_datatype(['string', 1]), StringType)
|
||||
assert isinstance(get_datatype(['string', 10, 1]), StringType)
|
||||
|
||||
@ -336,15 +335,15 @@ def test_get_datatype():
|
||||
get_datatype(['array', 1])
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['array', [1], 2, 3])
|
||||
assert isinstance(get_datatype(['array', ['blob', 1], 1]), ArrayOf)
|
||||
assert isinstance(get_datatype(['array', ['blob', 1], 1]).subtype, BLOBType)
|
||||
assert isinstance(get_datatype(['array', 1, 1, ['blob', 1]]), ArrayOf)
|
||||
assert isinstance(get_datatype(['array', 1, 1, ['blob', 1]]).subtype, BLOBType)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['array', ['blob', 1], -10])
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['array', ['blob', 1], 10, -10])
|
||||
|
||||
assert isinstance(get_datatype(['array', ['blob', 1], 10, 1]), ArrayOf)
|
||||
assert isinstance(get_datatype(['array', 1, 10, ['blob', 1]]), ArrayOf)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['tuple'])
|
||||
@ -352,16 +351,15 @@ def test_get_datatype():
|
||||
get_datatype(['tuple', 1])
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['tuple', [1], 2, 3])
|
||||
assert isinstance(get_datatype(['tuple', [['blob', 1]]]), TupleOf)
|
||||
assert isinstance(get_datatype(
|
||||
['tuple', [['blob', 1]]]).subtypes[0], BLOBType)
|
||||
assert isinstance(get_datatype(['tuple', ['blob', 1]]), TupleOf)
|
||||
assert isinstance(get_datatype(['tuple', ['blob', 1]]).subtypes[0], BLOBType)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['tuple', [['blob', 1]], -10])
|
||||
get_datatype(['tuple', ['blob', 1], -10])
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['tuple', [['blob', 1]], 10, -10])
|
||||
get_datatype(['tuple', ['blob', 1], 10, -10])
|
||||
|
||||
assert isinstance(get_datatype(['tuple', [['blob', 1], ['int']]]), TupleOf)
|
||||
assert isinstance(get_datatype(['tuple', ['blob', 1], ['int']]), TupleOf)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['struct'])
|
||||
@ -370,13 +368,12 @@ def test_get_datatype():
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['struct', [1], 2, 3])
|
||||
assert isinstance(get_datatype(['struct', {'blob': ['blob', 1]}]), StructOf)
|
||||
assert isinstance(get_datatype(
|
||||
['struct', {'blob': ['blob', 1]}]).named_subtypes['blob'], BLOBType)
|
||||
assert isinstance(get_datatype(['struct', {'blob': ['blob', 1]}]).named_subtypes['blob'], BLOBType)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['struct', [['blob', 1]], -10])
|
||||
get_datatype(['struct', ['blob', 1], -10])
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(['struct', [['blob', 1]], 10, -10])
|
||||
get_datatype(['struct', ['blob', 1], 10, -10])
|
||||
|
||||
assert isinstance(get_datatype(
|
||||
['struct', {'blob': ['blob', 1], 'int':['int']}]), StructOf)
|
||||
|
@ -69,13 +69,13 @@ def test_ModuleMeta():
|
||||
'param2': Parameter('param2', datatype=BoolType(), default=True),
|
||||
},
|
||||
"commands": {
|
||||
"cmd": Command('stuff',[BoolType()], BoolType())
|
||||
"cmd": Command('stuff',BoolType(), BoolType())
|
||||
},
|
||||
"accessibles": {
|
||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
||||
'value':Override(datatype=BoolType(), default = True),
|
||||
'cmd2': Command('another stuff', [BoolType()], BoolType()),
|
||||
'cmd2': Command('another stuff', BoolType(), BoolType()),
|
||||
},
|
||||
"do_cmd": lambda self, arg: not arg,
|
||||
"do_cmd2": lambda self, arg: not arg,
|
||||
|
@ -35,10 +35,10 @@ from secop.params import Command, Parameter, Override
|
||||
|
||||
|
||||
def test_Command():
|
||||
cmd = Command('do_something', [], None)
|
||||
cmd = Command('do_something')
|
||||
assert cmd.description
|
||||
assert cmd.ctr
|
||||
assert cmd.arguments == []
|
||||
assert cmd.argument is None
|
||||
assert cmd.result is None
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user