diff --git a/etc/cryo.cfg b/etc/cryo.cfg index db30633..63a5014 100644 --- a/etc/cryo.cfg +++ b/etc/cryo.cfg @@ -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 diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index 4ad605b..2668f80 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -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 , 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') diff --git a/secop/datatypes.py b/secop/datatypes.py index 84ab99d..7b15294 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -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): diff --git a/secop/errors.py b/secop/errors.py index 1c6e088..c51a7dc 100644 --- a/secop/errors.py +++ b/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 # # ***************************************************************************** -"""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, +) diff --git a/secop/features.py b/secop/features.py index c996f1f..7ed467e 100644 --- a/secop/features.py +++ b/secop/features.py @@ -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), } diff --git a/secop/gui/miniplot.py b/secop/gui/miniplot.py new file mode 100644 index 0000000..cc8c59a --- /dev/null +++ b/secop/gui/miniplot.py @@ -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 +# +# ***************************************************************************** + + +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) diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index 405e143..032f500 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -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) diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index 8eac3c0..fe75189 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -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 = '' diff --git a/secop/gui/params/__init__.py b/secop/gui/params/__init__.py index 7947471..d60f8d6 100644 --- a/secop/gui/params/__init__.py +++ b/secop/gui/params/__init__.py @@ -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( diff --git a/secop/gui/qt.py b/secop/gui/qt.py index 24fb943..8f74747 100644 --- a/secop/gui/qt.py +++ b/secop/gui/qt.py @@ -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) diff --git a/secop/gui/ui/parambuttons.ui b/secop/gui/ui/parambuttons.ui index 19dec40..d36e1ab 100644 --- a/secop/gui/ui/parambuttons.ui +++ b/secop/gui/ui/parambuttons.ui @@ -6,10 +6,16 @@ 0 0 - 730 + 464 33 + + + 0 + 33 + + Form @@ -37,7 +43,7 @@ - 256 + 128 0 @@ -57,7 +63,7 @@ - 256 + 128 0 diff --git a/secop/gui/ui/paramview.ui b/secop/gui/ui/paramview.ui index 5645b3a..9234e45 100644 --- a/secop/gui/ui/paramview.ui +++ b/secop/gui/ui/paramview.ui @@ -6,8 +6,8 @@ 0 0 - 238 - 121 + 100 + 100 diff --git a/secop/modules.py b/secop/modules.py index 83e691e..8f8ff6a 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -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() ), } diff --git a/secop/params.py b/secop/params.py index 01725dd..0dc852f 100644 --- a/secop/params.py +++ b/secop/params.py @@ -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 diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 951a0e1..da024c4 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -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): diff --git a/secop/protocol/errors.py b/secop/protocol/errors.py deleted file mode 100644 index c6afabc..0000000 --- a/secop/protocol/errors.py +++ /dev/null @@ -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 -# -# ***************************************************************************** -"""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, -) diff --git a/secop/protocol/interface/tcp.py b/secop/protocol/interface/tcp.py index 37f60a1..25978e1 100644 --- a/secop/protocol/interface/tcp.py +++ b/secop/protocol/interface/tcp.py @@ -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: diff --git a/secop/protocol/messages.py b/secop/protocol/messages.py index 9200065..69efbb9 100644 --- a/secop/protocol/messages.py +++ b/secop/protocol/messages.py @@ -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' # + +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, diff --git a/secop_demo/cryo.py b/secop_demo/cryo.py index 29b4ba5..5f5501d 100644 --- a/secop_demo/cryo.py +++ b/secop_demo/cryo.py @@ -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), ) diff --git a/secop_demo/modules.py b/secop_demo/modules.py index 4a812b3..5b826c0 100644 --- a/secop_demo/modules.py +++ b/secop_demo/modules.py @@ -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]), } diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index d9ab607..572c55c 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -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)), } diff --git a/test/test_client_baseclient.py b/test/test_client_baseclient.py index adb28e7..3de9530 100644 --- a/test/test_client_baseclient.py +++ b/test/test_client_baseclient.py @@ -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', } } diff --git a/test/test_datatypes.py b/test/test_datatypes.py index d75c470..1d4ddbd 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -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) diff --git a/test/test_modules.py b/test/test_modules.py index 271680a..9589825 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -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, diff --git a/test/test_params.py b/test/test_params.py index 0136018..4c98cb2 100644 --- a/test/test_params.py +++ b/test/test_params.py @@ -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