diff --git a/bin/secop-gui b/bin/secop-gui index d2e67e0..c9ae56a 100755 --- a/bin/secop-gui +++ b/bin/secop-gui @@ -22,9 +22,12 @@ # # ***************************************************************************** +from __future__ import print_function + import sys from os import path + # Add import path for inplace usage sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..'))) @@ -39,7 +42,17 @@ def main(argv=None): if argv is None: argv = sys.argv - if '-d' in argv: + if '-h' in argv or '--help' in argv: + print("Usage: secop-gui [-d] [-h] [host:[port]]") + print() + print("Option GNU long option Meaning") + print("-h --help Show this message") + print("-d --debug Enable debug output") + print() + print("if not given, host defaults to 'localhost' and port to 10767") + sys.exit(0) + + if '-d' in argv or '--debug' in argv: mlzlog.initLogging('gui', 'debug') else: mlzlog.initLogging('gui', 'info') diff --git a/etc/amagnet.cfg b/etc/amagnet.cfg new file mode 100644 index 0000000..97b0406 --- /dev/null +++ b/etc/amagnet.cfg @@ -0,0 +1,103 @@ +[equipment] +id=MLZ_amagnet(Garfield) +.visibility=expert +foo=bar + +[interface tcp] +interface=tcp +bindto=0.0.0.0 +bindport=10767 +# protocol to use for this interface +framing=eol +encoding=demo + +[device enable] +class=secop_mlz.entangle.NamedDigitalOutput +tangodevice='tango://amagnet.antares.frm2:10000/box/plc/_enable' +value.datatype=["enum", {'On':1,'Off':0}] +target.datatype=["enum", {'On':1,'Off':0}] +.description='Enables to Output of the Powersupply' +.visibility='advanced' + +[device polarity] +class=secop_mlz.entangle.NamedDigitalOutput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_polarity +value.datatype=["enum", {'+1':1,'0':0,'-1':-1}] +target.datatype=["enum", {'+1':1,'0':0,'-1':-1}] +.description=polarity (+/-) switch + + there is an interlock in the plc: + if there is current, switching polarity is forbidden + if polarity is short, powersupply is disabled +.visibility=advanced +comtries=50 + + +[device symmetry] +class=secop_mlz.entangle.NamedDigitalOutput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_symmetric +value.datatype=["enum",{'symmetric':1,'short':0, 'asymmetric':-1}] +target.datatype=["enum",{'symmetric':1,'short':0, 'asymmetric':-1}] +.description=par/ser switch selecting (a)symmetric mode + + symmetric is ser, asymmetric is par +.visibility=advanced + +[device T1] +class=secop_mlz.entangle.AnalogInput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_t1 +.description=Temperature1 of the coils system +#warnlimits=(0, 50) +#unit=degC + +[device T2] +class=secop_mlz.entangle.AnalogInput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_t2 +.description=Temperature2 of the coils system +#warnlimits=(0, 50) +#unit=degC + +[device T3] +class=secop_mlz.entangle.AnalogInput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_t3 +.description=Temperature3 of the coils system +#warnlimits=(0, 50) +#unit=degC + +[device T4] +class=secop_mlz.entangle.AnalogInput +tangodevice=tango://amagnet.antares.frm2:10000/box/plc/_t4 +.description=Temperature4 of the coils system +#warnlimits=(0, 50) +#unit=degC + +[device currentsource] +class=secop_mlz.entangle.PowerSupply +tangodevice=tango://amagnet.antares.frm2:10000/box/lambda/curr +.description=Device for the magnet power supply (current mode) +abslimits=(0,200) +speed=1 +ramp=60 +precision=0.02 +current=0 +voltage=10 +#unit=A +.visibility=advanced + +[device mf] +class=secop_mlz.amagnet.GarfieldMagnet +.description=magnetic field device, handling polarity switching and stuff +subdev_currentsource=currentsource +subdev_enable=enable +subdev_polswitch=polarity +subdev_symmetry=symmetry +#unit=T +userlimits=(-0.35, 0.35) +calibrationtable={'symmetric':[0.00186517, 0.0431937, -0.185956, 0.0599757, 0.194042], + 'short': [0.0, 0.0, 0.0, 0.0, 0.0], + 'asymmetric':[0.00136154, 0.027454, -0.120951, 0.0495289, 0.110689]} +.meaning=The magnetic field +.priority=100 +.visibility=user + +abslimits.default=0,0.4 diff --git a/requirements.txt b/requirements.txt index 1e2eaaf..0ca1e31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ #--extra-index-url https://forge.frm2.tum.de/simple +serial mlzlog >=0.2.0 # for generating docu markdown>=2.6 diff --git a/secop/client/__init__.py b/secop/client/__init__.py index 8015472..a0840b1 100644 --- a/secop/client/__init__.py +++ b/secop/client/__init__.py @@ -23,6 +23,8 @@ # nothing here yet. +from __future__ import print_function + import code @@ -47,16 +49,19 @@ class NameSpace(dict): dict.__delitem__(self, name) -import ConfigParser +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser def getClientOpts(cfgfile): parser = ConfigParser.SafeConfigParser() if not parser.read([cfgfile + '.cfg']): - print "Error reading cfg file %r" % cfgfile + print("Error reading cfg file %r" % cfgfile) return {} if not parser.has_section('client'): - print "No Server section found!" + print("No Server section found!") return dict(item for item in parser.items('client')) @@ -83,7 +88,7 @@ class ClientConsole(object): def helpCmd(self, arg=Ellipsis): if arg is Ellipsis: - print "No help available yet" + print("No help available yet") else: help(arg) diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index 91a3b95..f57240d 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -21,12 +21,20 @@ # ***************************************************************************** """Define Client side proxies""" +from __future__ import print_function + import json import socket import serial from select import select import threading -import Queue + +# Py2/3 +try: + import Queue +except ImportError: + import queue as Queue + from collections import OrderedDict import mlzlog @@ -71,14 +79,14 @@ class TCPConnection(object): if dlist[0] in rlist + wlist: newdata = self._io.recv(1024) if dlist[0] in xlist: - print "Problem: exception on socket, reconnecting!" + print("Problem: exception on socket, reconnecting!") for cb, arg in self.callbacks: cb(arg) return except socket.timeout: pass except Exception as err: - print err, "reconnecting" + print(err, "reconnecting") for cb, arg in self.callbacks: cb(arg) return @@ -260,7 +268,6 @@ class Client(object): if spec else "got expected reply '%s'" % msgtype) entry.extend([False, msgtype, spec, data]) entry[0].set() - return def encode_message(self, requesttype, spec='', data=None): """encodes the given message to a string @@ -292,12 +299,17 @@ class Client(object): def _handle_event(self, spec, data): """handles event""" - self.log.debug('handle_event %r %r' % (spec, data)) +# self.log.debug('handle_event %r %r' % (spec, data)) if ':' not in spec: self.log.warning("deprecated specifier %r" % spec) spec = '%s:value' % spec modname, pname = spec.split(':', 1) + previous = '' + if modname in self._cache: + if pname in self._cache: + previous = self._cache[modname][pname] self._cache.setdefault(modname, {})[pname] = Value(*data) +# self.log.info('cache: %s:%s=%r (was: %s)', modname, pname, data, previous) if spec in self.callbacks: for func in self.callbacks[spec]: try: @@ -351,6 +363,12 @@ class Client(object): ['parameters', 'commands'], module) self.describing_data = describing_data +# import pprint +# def r(stuff): +# if isinstance(stuff, dict): +# return dict((k,r(v)) for k,v in stuff.items()) +# return stuff +# pprint.pprint(r(describing_data)) for module, moduleData in self.describing_data['modules'].items(): for parameter, parameterData in moduleData[ @@ -359,7 +377,7 @@ class Client(object): self.describing_data['modules'][module]['parameters'] \ [parameter]['datatype'] = datatype except Exception as exc: - print formatException(verbose=True) + print(formatException(verbose=True)) raise def register_callback(self, module, parameter, cb): @@ -402,6 +420,10 @@ class Client(object): if msgtype == "*IDN?": return self.secop_id + # sanitize input + msgtype = str(msgtype) + spec = str(spec) + if msgtype not in ('*IDN?', 'describe', 'activate', 'deactivate', 'do', 'change', 'read', 'ping', 'help'): raise EXCEPTIONS['Protocol'](args=[ @@ -411,9 +433,7 @@ class Client(object): errorinfo='%r: No Such Messagetype defined!' % msgtype, ), ]) - # sanitize input + handle syntactic sugar - msgtype = str(msgtype) - spec = str(spec) + # handle syntactic sugar if msgtype == 'change' and ':' not in spec: spec = spec + ':target' if msgtype == 'read' and ':' not in spec: @@ -460,14 +480,6 @@ class Client(object): if self._thread and self._thread.is_alive(): self.thread.join(self._thread) - def handle_async(self, msg): - self.log.info("Got async update %r" % msg) - device = msg.device - param = msg.param - value = msg.value - self._cache.getdefault(device, {})[param] = value - # XXX: further notification-callbacks needed ??? - def startup(self, async=False): self._issueDescribe() # always fill our cache @@ -524,7 +536,7 @@ class Client(object): return self.getModuleProperties(module)['interface'] def getCommands(self, module): - return self.describing_data['modules'][module]['commands'].keys() + return self.describing_data['modules'][module]['commands'] def getProperties(self, module, parameter): return self.describing_data['modules'][module]['parameters'][parameter] diff --git a/secop/datatypes.py b/secop/datatypes.py index 46726be..c30c2df 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -32,6 +32,7 @@ __all__ = [ "BoolType", "EnumType", "BLOBType", "StringType", "TupleOf", "ArrayOf", "StructOf", + "Command", ] # base class for all DataTypes @@ -39,6 +40,7 @@ __all__ = [ class DataType(object): as_json = ['undefined'] + IS_COMMAND = False def validate(self, value): """validate a external representation and return an internal one""" @@ -139,7 +141,7 @@ class IntRange(DataType): return "IntRange(%d, %d)" % (self.min, self.max) if self.min is not None: return "IntRange(%d)" % self.min - return "IntRange(%d)" % self.min + return "IntRange()" def export(self, value): """returns a python object fit for serialisation""" @@ -159,7 +161,6 @@ class EnumType(DataType): num = 0 for arg in args: if not isinstance(arg, str): - print arg, type(arg) raise ValueError('EnumType entries MUST be strings!') self.entries[num] = arg num += 1 @@ -172,8 +173,8 @@ class EnumType(DataType): v, self.entries[v]) self.entries[v] = k - if len(self.entries) == 0: - raise ValueError('Empty enums ae not allowed!') +# if len(self.entries) == 0: +# raise ValueError('Empty enums ae not allowed!') self.reversed = {} for k, v in self.entries.items(): if v in self.reversed: @@ -442,7 +443,7 @@ class StructOf(DataType): if len(value.keys()) != len(self.named_subtypes.keys()): raise ValueError( 'Illegal number of Arguments! Need %d arguments.', len( - self.namd_subtypes.keys())) + self.named_subtypes.keys())) # validate elements and return as dict return dict((str(k), self.named_subtypes[k].validate(v)) for k, v in value.items()) @@ -463,6 +464,60 @@ class StructOf(DataType): return self.validate(dict(value)) +class Command(DataType): + IS_COMMAND = True + + def __init__(self, argtypes=[], resulttype=None): + for arg in argsin: + if not isinstance(arg, DataType): + raise ValueError('Command: Argument types must be DataTypes!') + if resulttype is not None: + if not isinstance(resulttype, DataType): + raise ValueError('Command: result type must be DataTypes!') + self.argtypes = argtypes + self.resulttype = resulttype + + if resulttype is not None: + self.as_json = ['command', + [t.as_json for t in argtypes], + resulttype.as_json] + else: + self.as_json = ['command', + [t.as_json for t in argtypes], + None] # XXX: or NoneType ??? + + def __repr__(self): + argstr = ', '.join(repr(arg) for arg in self.argtypes) + if self.resulttype is None: + return 'Command(%s)' % argstr + return 'Command(%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( + '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('Can not validate %s: %s', repr(value), str(exc)) + + def export(self, value): + """returns a python object fit for serialisation""" + if len(value) != len(self.argtypes): + raise ValueError( + 'Illegal number of Arguments! Need %d arguments.' % len( + self.argtypes)) +# return [t.export(v) for t,v in zip(self.argtypes, value)] + + def from_string(self, text): + import ast + value = ast.literal_eval(text) + return self.validate(value) + + # XXX: derive from above classes automagically! DATATYPES = dict( bool=lambda: BoolType(), @@ -476,6 +531,7 @@ DATATYPES = dict( enum=lambda kwds: EnumType(**kwds), struct=lambda named_subtypes: StructOf( **dict((n, get_datatype(t)) for n, t in named_subtypes.items())), + command=Command, ) diff --git a/secop/errors.py b/secop/errors.py index 709f1af..c1c4d81 100644 --- a/secop/errors.py +++ b/secop/errors.py @@ -39,42 +39,54 @@ class ProgrammingError(SECoPServerError): class SECoPError(SECoPServerError): errorclass = 'InternalError' + class NoSuchModuleError(SECoPError): errorclass = 'NoSuchModule' + class NoSuchParameterError(SECoPError): errorclass = 'NoSuchParameter' + class NoSuchCommandError(SECoPError): errorclass = 'NoSuchCommand' + class CommandFailedError(SECoPError): errorclass = 'CommandFailed' + class CommandRunningError(SECoPError): errorclass = 'CommandRunning' + class ReadOnlyError(SECoPError): errorclass = 'ReadOnly' + class BadValueError(SECoPError): errorclass = 'BadValue' + class CommunicationError(SECoPError): errorclass = 'CommunicationFailed' + class TimeoutError(SECoPError): errorclass = 'CommunicationFailed' # XXX: add to SECop messages + class HardwareError(SECoPError): errorclass = 'CommunicationFailed' # XXX: Add to SECoP messages + class IsBusyError(SECoPError): errorclass = 'IsBusy' + class IsErrorError(SECoPError): errorclass = 'IsError' + class DisabledError(SECoPError): errorclass = 'Disabled' - diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py index 84d33a7..cee2e2d 100644 --- a/secop/gui/mainwindow.py +++ b/secop/gui/mainwindow.py @@ -21,6 +21,8 @@ # # ***************************************************************************** +from __future__ import print_function + from PyQt4.QtGui import QMainWindow, QInputDialog, QTreeWidgetItem, QMessageBox from PyQt4.QtCore import pyqtSignature as qtsig, QObject, pyqtSignal @@ -91,7 +93,7 @@ class MainWindow(QMainWindow): try: self._addNode(host) except Exception as e: - print e + print(e) @qtsig('') def on_actionAdd_SEC_node_triggered(self): @@ -108,11 +110,11 @@ class MainWindow(QMainWindow): 'Connecting to %s failed!' % host, str(e)) def on_validateCheckBox_toggled(self, state): - print "validateCheckBox_toggled", state + print("validateCheckBox_toggled", state) def on_visibilityComboBox_activated(self, level): if level in ['user', 'admin', 'expert']: - print "visibility Level now:", level + print("visibility Level now:", level) def on_treeWidget_currentItemChanged(self, current, previous): if current.type() == ITEM_TYPE_NODE: diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index fc2782f..dfe2773 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -21,38 +21,13 @@ # # ***************************************************************************** -from PyQt4.QtGui import QWidget, QLabel, QMessageBox, QCheckBox +from __future__ import print_function + +from PyQt4.QtGui import QWidget, QLabel, QPushButton as QButton, QLineEdit, QMessageBox, QCheckBox, QSizePolicy from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal from secop.gui.util import loadUi - - -class ParameterButtons(QWidget): - setRequested = pyqtSignal(str, str, str) # module, parameter, target - - def __init__(self, - module, - parameter, - initval='', - readonly=True, - parent=None): - super(ParameterButtons, self).__init__(parent) - loadUi(self, 'parambuttons.ui') - - self._module = module - self._parameter = parameter - - self.currentLineEdit.setText(str(initval)) - if readonly: - self.setPushButton.setEnabled(False) - self.setLineEdit.setEnabled(False) - else: - self.setLineEdit.returnPressed.connect( - self.on_setPushButton_clicked) - - def on_setPushButton_clicked(self): - self.setRequested.emit(self._module, self._parameter, - self.setLineEdit.text()) +from secop.gui.params import ParameterView class ParameterGroup(QWidget): @@ -79,7 +54,7 @@ class ParameterGroup(QWidget): self._row += 1 def on_toggle_clicked(self): - print "ParameterGroup.on_toggle_clicked" + print("ParameterGroup.on_toggle_clicked") if self.paramGroupBox.isChecked(): for w in self._widgets: w.show() @@ -122,6 +97,9 @@ class ModuleCtrl(QWidget): if group is not None: allGroups.add(group) paramsByGroup.setdefault(group, []).append(param) + # enforce reading initial value if not already in cache + if param not in initValues: + self._node.getParameter(self._module, param) groupWidgets = {} # groupname -> CheckBoxWidget for (un)folding @@ -137,9 +115,12 @@ class ModuleCtrl(QWidget): # check if there is a param of the same name too if group in params: + datatype = self._node.getProperties( + self._module, group).get( + 'datatype', None) # yes: create a widget for this as well labelstr, buttons = self._makeEntry( - param, initValues[param].value, nolabel=True, checkbox=checkbox, invert=True) + param, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True) checkbox.setText(labelstr) # add to Layout (yes: ignore the label!) @@ -150,11 +131,19 @@ class ModuleCtrl(QWidget): row += 1 # loop over all params and insert and connect - for param in paramsByGroup[param]: - if param == group: + for param_ in paramsByGroup[param]: + if param_ == group: continue + if param_ not in initValues: + initval = None + print("Warning: %r not in initValues!" % param_) + else: + initval = initValues[param_].value + datatype = self._node.getProperties( + self._module, param_).get( + 'datatype', None) label, buttons = self._makeEntry( - param, initValues[param].value, checkbox=checkbox, invert=False) + param_, initval, checkbox=checkbox, invert=False) # add to Layout self.paramGroupBox.layout().addWidget(label, row, 0) @@ -166,18 +155,43 @@ class ModuleCtrl(QWidget): # or is named after a group (otherwise its created above) props = self._node.getProperties(self._module, param) if props.get('group', param) == param: + datatype = self._node.getProperties( + self._module, param).get( + 'datatype', None) label, buttons = self._makeEntry( - param, initValues[param].value) + param, initValues[param].value, datatype=datatype) # add to Layout self.paramGroupBox.layout().addWidget(label, row, 0) self.paramGroupBox.layout().addWidget(buttons, row, 1) row += 1 + # also populate properties + self._propWidgets = {} + props = self._node.getModuleProperties(self._module) + row = 0 + for prop in sorted(props): + label = QLabel(prop + ':') + label.setFont(self._labelfont) + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + + # make 'display' label + view = QLabel(str(props[prop])) + view.setFont(self.font()) + view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + view.setWordWrap(True) + + self.propertyGroupBox.layout().addWidget(label, row, 0) + self.propertyGroupBox.layout().addWidget(view, row, 1) + row += 1 + + self._propWidgets[prop] = (label, view) + def _makeEntry( self, param, initvalue, + datatype=None, nolabel=False, checkbox=None, invert=False): @@ -194,8 +208,12 @@ class ModuleCtrl(QWidget): if checkbox and not invert: labelstr = ' ' + labelstr - buttons = ParameterButtons( - self._module, param, initvalue, props['readonly']) + buttons = ParameterView( + self._module, + param, + datatype=datatype, + initvalue=initvalue, + readonly=props['readonly']) buttons.setRequested.connect(self._set_Button_pressed) if description: @@ -243,5 +261,4 @@ class ModuleCtrl(QWidget): def _updateValue(self, module, parameter, value): if module != self._module: return - - self._paramWidgets[parameter][1].currentLineEdit.setText(str(value[0])) + self._paramWidgets[parameter][1].updateValue(str(value[0])) diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index fb4e8d0..ac48656 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -24,11 +24,12 @@ import pprint import json -from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics -from PyQt4.QtCore import pyqtSignature as qtsig, Qt +from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics, QLabel, QPushButton, QLineEdit, QMessageBox, QCheckBox, QSizePolicy +from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal from secop.gui.util import loadUi from secop.protocol.errors import SECOPError +from secop.datatypes import StringType, EnumType class NodeCtrl(QWidget): @@ -44,6 +45,9 @@ class NodeCtrl(QWidget): self.protocolVersionLabel.setText(self._node.protocolVersion) self._clearLog() + # now populate modules tab + self._init_modules_tab() + @qtsig('') def on_sendPushButton_clicked(self): msg = self.msgLineEdit.text().strip() @@ -118,3 +122,172 @@ class NodeCtrl(QWidget): # due to monospace) result = self.logTextBrowser.width() / fontMetrics.width('a') return result + + def _init_modules_tab(self): + self._moduleWidgets = [] + layout = self.scrollAreaWidgetContents.layout() + labelfont = self.font() + labelfont.setBold(True) + row = 0 + for modname in sorted(self._node.modules): + modprops = self._node.getModuleProperties(modname) + baseclass = modprops['interface'] + description = modprops['interface'] + unit = self._node.getProperties(modname, 'value').get('unit', '') + + if unit: + labelstr = '%s (%s):' % (modname, unit) + else: + labelstr = '%s:' % (modname,) + label = QLabel(labelstr) + label.setFont(labelfont) + + if baseclass == 'Driveable': + widget = DriveableWidget(self._node, modname, self) + elif baseclass == 'Readable': + widget = ReadableWidget(self._node, modname, self) + else: + widget = QLabel('Unsupported Interfaceclass %r' % baseclass) + + if description: + widget.setToolTip(description) + + layout.addWidget(label, row, 0) + layout.addWidget(widget, row, 1) + + row += 1 + self._moduleWidgets.extend((label, widget)) + + +class ReadableWidget(QWidget): + + def __init__(self, node, module, parent=None): + super(ReadableWidget, self).__init__(parent) + self._node = node + self._module = module + + params = self._node.getProperties(self._module, 'value') + datatype = params.get('datatype', StringType()) + self._is_enum = isinstance(datatype, EnumType) + + loadUi(self, 'modulebuttons.ui') + + # populate comboBox, keeping a mapping of Qt-index to EnumValue + if self._is_enum: + self._map = {} # maps QT-idx to name/value + self._revmap = {} # maps value/name to QT-idx + for idx, (val, name) in enumerate( + sorted(datatype.entries.items())): + self._map[idx] = (name, val) + self._revmap[name] = idx + self._revmap[val] = idx + self.targetComboBox.addItem(name, val) + + self._init_status_widgets() + self._init_current_widgets() + self._init_target_widgets() + + self._node.newData.connect(self._updateValue) + + def _get(self, pname, fallback=Ellipsis): + params = self._node.queryCache(self._module) + if pname in params: + return params[pname].value + try: + return self._node.getParameter(self._module, pname) + except Exception: + self.log.exception() + if fallback is not Ellipsis: + return fallback + raise + + def _init_status_widgets(self): + self.update_status(self._get('status', (999, ''))) + # XXX: also connect update_status signal to LineEdit ?? + + def update_status(self, status, qualifiers={}): + self.statusLineEdit.setText(str(status)) + # may change meaning of cmdPushButton + + def _init_current_widgets(self): + self.update_current(self._get('value', '')) + + def update_current(self, value, qualifiers={}): + self.currentLineEdit.setText(str(value)) + + def _init_target_widgets(self): + # Readable has no target: disable widgets + self.targetLineEdit.setHidden(True) + self.targetComboBox.setHidden(True) + self.cmdPushButton.setHidden(True) + + def update_target(self, target, qualifiers={}): + pass + + def target_go(self, target): + try: + self._node.setParameter(self._module, 'target', target) + except Exception as e: + QMessageBox.warning(self.parent(), 'Operation failed', str(e)) + + def _updateValue(self, module, parameter, value): + if module != self._module: + return + if parameter == 'status': + self.update_status(*value) + elif parameter == 'value': + self.update_current(*value) + elif parameter == 'target': + self.update_target(*value) + + +class DriveableWidget(ReadableWidget): + + def _init_target_widgets(self): + params = self._node.getProperties(self._module, 'target') + if self._is_enum: + # EnumType: disable Linedit + self.targetLineEdit.setHidden(True) + else: + # normal types: disable Combobox + self.targetComboBox.setHidden(True) + target = self._get('target', None) + if target: + if isinstance(target, list) and isinstance(target[1], dict): + self.update_target(target[0]) + else: + self.update_target(target) + + def update_current(self, value, qualifiers={}): + if self._is_enum: + self.currentLineEdit.setText(self._map[self._revmap[value]][0]) + else: + self.currentLineEdit.setText(str(value)) + + def update_target(self, target, qualifiers={}): + if self._is_enum: + # update selected item + if target in self._revmap: + self.targetComboBox.setCurrentIndex(self._revmap[target]) + else: + print( + "%s: Got invalid target value %r!" % + (self._module, target)) + else: + self.targetLineEdit.setText(str(target)) + + def on_cmdPushButton_clicked(self, toggle=False): + if toggled: + return + if self._is_enum: + self.on_targetComboBox_activated() + else: + self.on_targetLineEdit_returnPressed() + + def on_targetLineEdit_returnPressed(self): + self.target_go(self.targetLineEdit.text()) + + def on_targetComboBox_activated(self, stuff=''): + if isinstance(stuff, (str, unicode)): + return + self.target_go(self._map[self.targetComboBox.currentIndex()][0]) diff --git a/secop/gui/params/__init__.py b/secop/gui/params/__init__.py new file mode 100644 index 0000000..718e540 --- /dev/null +++ b/secop/gui/params/__init__.py @@ -0,0 +1,182 @@ +# -*- 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 PyQt4.QtGui import QWidget, QLabel, QPushButton as QButton, QLineEdit, QMessageBox, QCheckBox, QSizePolicy +from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal + +from secop.gui.util import loadUi +from secop.datatypes import * + + +class ParameterWidget(QWidget): + setRequested = pyqtSignal(str, str, str) # module, parameter, target + cmdRequested = pyqtSignal(str, str, list) # module, command, args + + def __init__(self, + module, + paramcmd, + datatype=None, + initvalue=None, + readonly=True, + parent=None): + super(ParameterWidget, self).__init__(parent) + self._module = module + self._paramcmd = paramcmd + self._datatype = datatype + self._readonly = readonly + + self._load_ui(initvalue) + + def _load_ui(self, initvalue): + # load ui file, set initvalue to right widget + pass + + def updateValue(self, valuestr): + # async ! + pass + + +class GenericParameterWidget(ParameterWidget): + + def _load_ui(self, initvalue): + # using two QLineEdits for current and target value + loadUi(self, 'parambuttons.ui') + + if self._readonly: + self.setPushButton.setEnabled(False) + self.setLineEdit.setEnabled(False) + else: + self.setLineEdit.returnPressed.connect( + self.on_setPushButton_clicked) + self.updateValue(str(initvalue)) + + def on_setPushButton_clicked(self): + self.setRequested.emit(self._module, self._paramcmd, + self.setLineEdit.text()) + + def updateValue(self, valuestr): + self.currentLineEdit.setText(valuestr) + + +class EnumParameterWidget(GenericParameterWidget): + + def _load_ui(self, initvalue): + # using two QLineEdits for current and target value + loadUi(self, 'parambuttons_select.ui') + + # transfer allowed settings from datatype to comboBoxes + self._map = {} # maps index to enumstring + self._revmap = {} # maps enumstring to index + index = 0 + for data, entry in sorted(self._datatype.entries.items()): + self.setComboBox.addItem(entry, data) + self._map[index] = entry + self._revmap[entry] = index + self._revmap[data] = index + index += 1 + if self._readonly: + self.setLabel.setEnabled(False) + self.setComboBox.setEnabled(False) + self.setLabel.setHidden(True) + self.setComboBox.setHidden(True) + else: + self.setComboBox.activated.connect(self.on_setPushButton_clicked) + + self.updateValue(str(initvalue)) + + def on_setPushButton_clicked(self): + self.setRequested.emit( + self._module, self._paramcmd, str( + self._datatype.reversed[ + self._map[ + self.setComboBox.currentIndex()]])) + + def updateValue(self, valuestr): + try: + self.currentLineEdit.setText( + self._datatype.entries.get( + int(valuestr), valuestr)) + except ValueError: + self.currentLineEdit.setText('undefined Value: %r' % valuestr) + + +class GenericCmdWidget(ParameterWidget): + + def _load_ui(self, initvalue): + # using two QLineEdits for current and target value + loadUi(self, 'cmdbuttons.ui') + + self.cmdLineEdit.setText('') + self.cmdLineEdit.setEnabled(self.datatype.argtypes is not None) + self.cmdLineEdit.returnPressed.connect( + self.on_cmdPushButton_clicked) + + def on_cmdPushButton_clicked(self): + # wait until command complete before retrying + self.cmdPushButton.setEnabled(False) + self.cmdRequested.emit( + self._module, + self._paramcmd, + self._datatype.from_string( + self.cmdLineEdit.text())) + + def updateValue(self, valuestr): + # open dialog and show value, if any. + # then re-activate the command button + self.cmdPushButton.setEnabled(True) + + +def ParameterView(module, + paramcmd, + datatype=None, + initvalue=None, + readonly=True, + 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( + module, + paramcmd, # name of command + datatype, + initvalue, # not used for comands + readonly, # not used for commands + parent) + if isinstance(datatype, EnumType): + return EnumParameterWidget( + module, + paramcmd, # name of parameter + datatype, + initvalue, + readonly, + parent) + + return GenericParameterWidget( + module, + paramcmd, # name of parameter + datatype, + initvalue, + readonly, + parent) diff --git a/secop/gui/ui/cmdbuttons.ui b/secop/gui/ui/cmdbuttons.ui new file mode 100644 index 0000000..b1c010e --- /dev/null +++ b/secop/gui/ui/cmdbuttons.ui @@ -0,0 +1,60 @@ + + + Form + + + + 0 + 0 + 730 + 33 + + + + Form + + + + 6 + + + 0 + + + 0 + + + + + Arguments: + + + + + + + true + + + + 256 + 0 + + + + true + + + + + + + Go + + + + + + + + diff --git a/secop/gui/ui/mainwindow.ui b/secop/gui/ui/mainwindow.ui index 78e6e76..9e42e33 100644 --- a/secop/gui/ui/mainwindow.ui +++ b/secop/gui/ui/mainwindow.ui @@ -29,6 +29,9 @@ + + false + user @@ -61,6 +64,9 @@ + + false + Validate locally @@ -104,7 +110,7 @@ 0 0 1228 - 23 + 33 diff --git a/secop/gui/ui/modulebuttons.ui b/secop/gui/ui/modulebuttons.ui new file mode 100644 index 0000000..cce4687 --- /dev/null +++ b/secop/gui/ui/modulebuttons.ui @@ -0,0 +1,79 @@ + + + Form + + + + 0 + 0 + 748 + 74 + + + + Form + + + + 6 + + + 0 + + + 6 + + + 0 + + + 0 + + + + + true + + + + 256 + 0 + + + + true + + + + + + + 0 + + + + + + + + + + + + + Go + + + + + + + true + + + + + + + + diff --git a/secop/gui/ui/modulectrl.ui b/secop/gui/ui/modulectrl.ui index 33dcbfa..0978226 100644 --- a/secop/gui/ui/modulectrl.ui +++ b/secop/gui/ui/modulectrl.ui @@ -6,53 +6,15 @@ 0 0 - 230 - 195 + 257 + 162 Form - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - 75 - false - true - - - - Module name: - - - - - - - TextLabel - - - - - - + Parameters: @@ -76,44 +38,7 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - Properties: - - - - 0 - - - 6 - - - 0 - - - 6 - - - - - + Qt::Vertical @@ -126,6 +51,54 @@ + + + + Properties: + + + + + + + + + + + 75 + false + true + + + + Module name: + + + + + + + + 18 + 75 + true + + + + TextLabel + + + + + + + + + Commands: + + + + diff --git a/secop/gui/ui/nodectrl.ui b/secop/gui/ui/nodectrl.ui index f0b1c43..f26005e 100644 --- a/secop/gui/ui/nodectrl.ui +++ b/secop/gui/ui/nodectrl.ui @@ -85,49 +85,91 @@ - - - - - - - - >>> - - - - - - - Send - - - false - - - false - - - - - - - Clear - - - - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> + + + 1 + + + + Console + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - - - - +</style></head><body style=" font-family:'Noto Sans'; font-size:12pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans'; font-size:11pt;"><br /></p></body></html> + + + + + + + + + Clear + + + + + + + + + + >>> + + + + + + + Send + + + false + + + false + + + + + + + + + + true + + + Modules + + + + + + true + + + + + 0 + 0 + 610 + 324 + + + + + + + + + diff --git a/secop/gui/ui/parambuttons_select.ui b/secop/gui/ui/parambuttons_select.ui index cea746b..6f5be24 100644 --- a/secop/gui/ui/parambuttons_select.ui +++ b/secop/gui/ui/parambuttons_select.ui @@ -7,7 +7,7 @@ 0 0 730 - 33 + 39 @@ -36,25 +36,15 @@ - - - - Current: - - - - - - - Set: - - - - - - - + + + + 0 + 0 + + + @@ -69,6 +59,30 @@ + + + + Current: + + + + + + + Set: + + + + + + + true + + + true + + + diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py index db7e128..7a03f39 100644 --- a/secop/lib/__init__.py +++ b/secop/lib/__init__.py @@ -223,10 +223,10 @@ def getfqdn(name=''): return socket.getfqdn(name) -if __name__ == '__main__': - print "minimal testing: lib" - d = attrdict(a=1, b=2) - _ = d.a + d['b'] - d.c = 9 - d['d'] = 'c' - assert d[d.d] == 9 +# if __name__ == '__main__': +# print "minimal testing: lib" +# d = attrdict(a=1, b=2) +# _ = d.a + d['b'] +# d.c = 9 +# d['d'] = 'c' +# assert d[d.d] == 9 diff --git a/secop/lib/parsing.py b/secop/lib/parsing.py index f2f03a8..cf0edc6 100644 --- a/secop/lib/parsing.py +++ b/secop/lib/parsing.py @@ -177,7 +177,6 @@ class ArgsParser(object): self.length = len(string) def setstring(self, string): - print repr(string) self.string = string self.idx = 0 self.length = len(string) @@ -191,7 +190,6 @@ class ArgsParser(object): def get(self): res = self.peek() self.idx += 1 - print "get->", res return res def skip(self): @@ -222,17 +220,14 @@ class ArgsParser(object): idx = self.idx res = self.parse_array() if res: - print "is Array" return res self.idx = idx res = self.parse_record() if res: - print "is record" return res self.idx = idx res = self.parse_string() if res: - print "is string" return res self.idx = idx return self.parse_number() @@ -388,26 +383,26 @@ def parse_args(s): __ALL__ = ['format_time', 'parse_time', 'parse_args'] -if __name__ == '__main__': - print "minimal testing: lib/parsing:" - print "time_formatting:", - t = time.time() - s = format_time(t) - assert (abs(t - parse_time(s)) < 1e-6) - print "OK" +# if __name__ == '__main__': +# print "minimal testing: lib/parsing:" +# print "time_formatting:", +# t = time.time() +# s = format_time(t) +# assert (abs(t - parse_time(s)) < 1e-6) +# print "OK"# +# +# print "ArgsParser:" +# a = ArgsParser() +# print a.parse('[ "\'\\\"A" , "<>\'", \'",C\', [1.23e1, 123.0e-001] , ]') - print "ArgsParser:" - a = ArgsParser() - print a.parse('[ "\'\\\"A" , "<>\'", \'",C\', [1.23e1, 123.0e-001] , ]') +# #import pdb +# #pdb.run('print a.parse()', globals(), locals()) - #import pdb - #pdb.run('print a.parse()', globals(), locals()) - - print "args_formatting:", - for obj in [1, 2.3, 'X', (1, 2, 3), [1, (3, 4), 'X,y']]: - s = format_args(obj) - p = a.parse(s) - print p, - assert (parse_args(format_args(obj)) == obj) - print "OK" - print "OK" +# print "args_formatting:", +# for obj in [1, 2.3, 'X', (1, 2, 3), [1, (3, 4), 'X,y']]: +# s = format_args(obj) +# p = a.parse(s) +# print p, +# assert (parse_args(format_args(obj)) == obj) +# print "OK" +# print "OK" diff --git a/secop/lib/sequence.py b/secop/lib/sequence.py index 9b48622..a5b6efd 100644 --- a/secop/lib/sequence.py +++ b/secop/lib/sequence.py @@ -36,6 +36,7 @@ class Namespace(object): class Step(object): + def __init__(self, desc, waittime, func, *args, **kwds): self.desc = desc self.waittime = waittime @@ -126,7 +127,7 @@ class SequencerMixin(object): """Can be called to check if a sequence is currently running.""" return self._seq_thread and self._seq_thread.isAlive() - def read_status(self): + def read_status(self, maxage=0): if self.seq_is_alive(): return status.BUSY, 'moving: ' + self._seq_phase elif self._seq_error: @@ -138,7 +139,7 @@ class SequencerMixin(object): return status.ERROR, self._seq_stopped return status.WARN, self._seq_stopped if hasattr(self, 'read_hw_status'): - return self.read_hw_status() + return self.read_hw_status(maxage) return OK, '' def do_stop(self): @@ -151,6 +152,9 @@ class SequencerMixin(object): except Exception as e: self.log.exception('unhandled error in sequence thread: %s', e) self._seq_error = str(e) + finally: + self._seq_thread = None + self.poll(0) def _seq_thread_inner(self, seq, store_init): store = Namespace() @@ -163,19 +167,24 @@ class SequencerMixin(object): try: while True: result = step.func(store, *step.args) - if self._seq_.stopflag: + if self._seq_stopflag: if result: self._seq_stopped = 'stopped while %s' % step.desc else: self._seq_stopped = 'stopped after %s' % step.desc cleanup_func = step.kwds.get('cleanup', None) if callable(cleanup_func): - cleanup_func(store, *step.args) + try: + cleanup_func(store, result, *step.args) + except Exception as e: + self.log.exception(e) + raise return sleep(step.waittime) if not result: break except Exception as e: - self.log.exception('error in sequence step: %s', e) + self.log.exception( + 'error in sequence step %r: %s', step.desc, e) self._seq_error = 'during %s: %s' % (step.desc, e) break diff --git a/secop/modules.py b/secop/modules.py index 9249ef5..a8a2110 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -31,11 +31,11 @@ import types import inspect import threading -from secop.lib import formatExtendedStack +from secop.lib import formatExtendedStack, mkthread from secop.lib.parsing import format_time from secop.errors import ConfigError, ProgrammingError from secop.protocol import status -from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, export_datatype +from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, export_datatype, get_datatype EVENT_ONLY_ON_CHANGED_VALUES = False @@ -72,6 +72,7 @@ class PARAM(object): self.readonly = readonly self.export = export self.group = group + # note: auto-converts True/False to 1/0 which yield the expected # behaviour... self.poll = int(poll) @@ -214,7 +215,9 @@ class DeviceMeta(type): else: # return cached value self.log.debug("rfunc(%s): return cached value" % pname) - return self.PARAMS[pname].value + value = self.PARAMS[pname].value + setattr(self, pname, value) + return value if rfunc: wrapped_rfunc.__doc__ = rfunc.__doc__ @@ -300,10 +303,13 @@ class Device(object): 'meaning': None, # XXX: ??? 'priority': None, # XXX: ??? 'visibility': None, # XXX: ???? + 'description': "The manufacturer forgot to set a meaningful description. please nag him!", # what else? } # PARAMS and CMDS are auto-merged upon subclassing - PARAMS = {} +# PARAMS = { +# 'description': PARAM('short description of this module and its function', datatype=StringType(), default='no specified'), +# } CMDS = {} DISPATCHER = None @@ -318,6 +324,12 @@ class Device(object): params[k] = v.copy() self.PARAMS = params + # make local copies of PROPERTIES + props = {} + for k, v in self.PROPERTIES.items()[:]: + props[k] = v + + self.PROPERTIES = props # check and apply properties specified in cfgdict # moduleproperties are to be specified as @@ -347,7 +359,9 @@ class Device(object): paramname, propname = k.split('.', 1) if paramname in self.PARAMS: paramobj = self.PARAMS[paramname] - if hasattr(paramobj, propname): + if propname == 'datatype': + paramobj.datatype = get_datatype(cfgdict.pop(k)) + elif hasattr(paramobj, propname): setattr(paramobj, propname, v) del cfgdict[k] @@ -355,8 +369,10 @@ class Device(object): # only accept config items specified in PARAMS for k, v in cfgdict.items(): if k not in self.PARAMS: - raise ConfigError('Device %s:config Parameter %r ' - 'not unterstood!' % (self.name, k)) + raise ConfigError( + 'Device %s:config Parameter %r ' + 'not unterstood! (use on of %r)' % + (self.name, k, self.PARAMS.keys())) # complain if a PARAM entry has no default value and # is not specified in cfgdict for k, v in self.PARAMS.items(): @@ -385,6 +401,7 @@ class Device(object): v = datatype.validate(v) except (ValueError, TypeError) as e: self.log.exception(formatExtendedStack()) + raise raise ConfigError('Device %s: config parameter %r:\n%r' % (self.name, k, e)) setattr(self, k, v) @@ -393,6 +410,10 @@ class Device(object): def init(self): # may be overriden in derived classes to init stuff self.log.debug('empty init()') + mkthread(self.late_init) + + def late_init(self): + self.log.debug('late init()') class Readable(Device): @@ -402,7 +423,7 @@ class Readable(Device): """ PARAMS = { 'value': PARAM('current value of the device', readonly=True, default=0., - datatype=FloatRange(), poll=True), + datatype=FloatRange(), unit='', poll=True), 'pollinterval': PARAM('sleeptime between polls', default=5, readonly=False, datatype=FloatRange(0.1, 120), ), 'status': PARAM('current status of the device', default=(status.OK, ''), @@ -427,25 +448,43 @@ class Readable(Device): def __pollThread(self): """super simple and super stupid per-module polling thread""" i = 0 + fastpoll = True # first update should be quick while True: i = 1 try: - time.sleep(self.pollinterval) + time.sleep(self.pollinterval * (0.1 if fastpoll else 1)) except TypeError: - time.sleep(max(self.pollinterval)) - try: - self.poll(i) - except Exception: # really ALL - pass + time.sleep(min(self.pollinterval) + if fastpoll else max(self.pollinterval)) + fastpoll = self.poll(i) def poll(self, nr): + # poll status first + fastpoll = False + if 'status' in self.PARAMS: + stat = self.read_status(0) +# self.log.info('polling read_status -> %r' % (stat,)) + fastpoll = stat[0] == status.BUSY +# if fastpoll: +# self.log.info('fastpoll!') for pname, pobj in self.PARAMS.iteritems(): if not pobj.poll: continue - if 0 == nr % int(pobj.poll): + if pname == 'status': + # status was already polled above + continue + if ((int(pobj.poll) < 0) and fastpoll) or ( + 0 == nr % abs(int(pobj.poll))): + # poll always if pobj.poll is negative and fastpoll (i.e. device is busy) + # otherwise poll every 'pobj.poll' iteration rfunc = getattr(self, 'read_' + pname, None) if rfunc: - rfunc() + try: + # self.log.info('polling read_%s -> %r' % (pname, rfunc())) + rfunc() + except Exception: # really all! + pass + return fastpoll class Driveable(Readable): diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 9578074..1532120 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -269,7 +269,7 @@ class Dispatcher(object): # now call func and wrap result as value # note: exceptions are handled in handle_request, not here! - func = getattr(moduleobj, 'do' + command) + func = getattr(moduleobj, 'do_' + command) res = func(*arguments) res = CommandReply( module=modulename, @@ -319,12 +319,14 @@ class Dispatcher(object): # note: exceptions are handled in handle_request, not here! readfunc() if pobj.timestamp: - return Value( + res = Value( modulename, parameter=pname, value=pobj.export_value, t=pobj.timestamp) - return Value(modulename, parameter=pname, value=pobj.export_value) + else: + res = Value(modulename, parameter=pname, value=pobj.export_value) + return res # now the (defined) handlers for the different requests def handle_Help(self, conn, msg): @@ -404,6 +406,10 @@ class Dispatcher(object): unit=pobj.unit) if res.value != Ellipsis: # means we do not have a value at all so skip this self.broadcast_event(res) + else: + self.log.error( + 'activate: got no value for %s:%s!' % + modulename, pname) conn.queue_async_reply(ActivateReply(**msg.as_dict())) return None diff --git a/secop/protocol/encoding/__init__.py b/secop/protocol/encoding/__init__.py index 5c1ee47..61e95bc 100644 --- a/secop/protocol/encoding/__init__.py +++ b/secop/protocol/encoding/__init__.py @@ -39,12 +39,12 @@ class MessageEncoder(object): raise NotImplemented -from demo_v2 import DemoEncoder as DemoEncoderV2 -from demo_v3 import DemoEncoder as DemoEncoderV3 -from demo_v4 import DemoEncoder as DemoEncoderV4 -from text import TextEncoder -from pickle import PickleEncoder -from simplecomm import SCPEncoder +from .demo_v2 import DemoEncoder as DemoEncoderV2 +from .demo_v3 import DemoEncoder as DemoEncoderV3 +from .demo_v4 import DemoEncoder as DemoEncoderV4 +from .text import TextEncoder +from .pickle import PickleEncoder +from .simplecomm import SCPEncoder ENCODERS = { 'pickle': PickleEncoder, diff --git a/secop/protocol/encoding/demo_v2.py b/secop/protocol/encoding/demo_v2.py index a0b90fd..4fe5df1 100644 --- a/secop/protocol/encoding/demo_v2.py +++ b/secop/protocol/encoding/demo_v2.py @@ -24,6 +24,8 @@ # implement as class as they may need some internal 'state' later on # (think compressors) +from __future__ import print_function + from secop.protocol.encoding import MessageEncoder from secop.protocol import messages from secop.lib.parsing import * @@ -43,9 +45,9 @@ class DemoEncoder(MessageEncoder): if match: novalue, devname, pname, propname, assign = match.groups() if assign: - print "parsing", assign, + print("parsing", assign,) assign = parse_args(assign) - print "->", assign + print("->", assign) return messages.DemoRequest(novalue, devname, pname, propname, assign) return messages.HelpRequest() @@ -56,13 +58,13 @@ class DemoEncoder(MessageEncoder): handler_name = '_encode_' + msg.__class__.__name__ handler = getattr(self, handler_name, None) if handler is None: - print "Handler %s not yet implemented!" % handler_name + print("Handler %s not yet implemented!" % handler_name) try: args = dict((k, msg.__dict__[k]) for k in msg.ARGS) result = handler(**args) except Exception as e: - print "Error encoding %r with %r!" % (msg, handler) - print e + print("Error encoding %r with %r!" % (msg, handler)) + print(e) return '~InternalError~' return result diff --git a/secop/protocol/encoding/demo_v3.py b/secop/protocol/encoding/demo_v3.py index a2899eb..c19e9c6 100644 --- a/secop/protocol/encoding/demo_v3.py +++ b/secop/protocol/encoding/demo_v3.py @@ -24,6 +24,8 @@ # implement as class as they may need some internal 'state' later on # (think compressors) +from __future__ import print_function + from secop.protocol.encoding import MessageEncoder from secop.protocol.messages import * from secop.protocol.errors import ProtocolError @@ -257,7 +259,7 @@ class DemoEncoder(MessageEncoder): mgroups['args'] = args # reformat qualifiers - print mgroups + print(mgroups) quals = dict( qual.split('=', 1) for qual in helper(mgroups.pop('qualifiers', ';'))) @@ -306,9 +308,9 @@ class DemoEncoder(MessageEncoder): 'read blub:c=14;t=3.3', ] for m in testmsg: - print repr(m) - print self.decode(m) - print + print(repr(m)) + print(self.decode(m)) + print() DEMO_RE_MZ = re.compile( @@ -326,7 +328,7 @@ class DemoEncoder_MZ(MessageEncoder): def decode(sef, encoded): m = DEMO_RE_MZ.match(encoded) if m: - print "implement me !" + print("implement me !") return HelpRequest() def encode(self, msg): diff --git a/secop/protocol/encoding/demo_v4.py b/secop/protocol/encoding/demo_v4.py index db1ced3..91a513e 100644 --- a/secop/protocol/encoding/demo_v4.py +++ b/secop/protocol/encoding/demo_v4.py @@ -24,6 +24,8 @@ # implement as class as they may need some internal 'state' later on # (think compressors) +from __future__ import print_function + from secop.lib.parsing import format_time from secop.protocol.encoding import MessageEncoder from secop.protocol.messages import * @@ -235,7 +237,7 @@ class DemoEncoder(MessageEncoder): # first check beginning match = DEMO_RE.match(encoded) if not match: - print repr(encoded), repr(IDENTREPLY) + print(repr(encoded), repr(IDENTREPLY)) if encoded == IDENTREPLY: # XXX:better just check the first 2 parts... return IdentifyReply(version_string=encoded) @@ -274,9 +276,9 @@ class DemoEncoder(MessageEncoder): origin=encoded) def tests(self): - print "---- Testing encoding -----" + print("---- Testing encoding -----") for msgclass, parts in sorted(self.ENCODEMAP.items()): - print msgclass + print(msgclass) e = self.encode( msgclass( module='', @@ -289,17 +291,17 @@ class DemoEncoder(MessageEncoder): nonce='', errorclass='InternalError', errorinfo='nix')) - print e - print self.decode(e) - print - print "---- Testing decoding -----" + print(e) + print(self.decode(e)) + print() + print("---- Testing decoding -----") for msgtype, _ in sorted(self.DECODEMAP.items()): msg = '%s a:b 3' % msgtype if msgtype == EVENT: msg = '%s a:b [3,{"t":193868}]' % msgtype - print msg + print(msg) d = self.decode(msg) - print d - print self.encode(d) - print - print "---- Testing done -----" + print(d) + print(self.encode(d)) + print() + print("---- Testing done -----") diff --git a/secop/protocol/errors.py b/secop/protocol/errors.py index 172181e..33054e3 100644 --- a/secop/protocol/errors.py +++ b/secop/protocol/errors.py @@ -91,9 +91,3 @@ EXCEPTIONS = dict( Readonly=ReadonlyError, CommandFailed=CommandFailedError, InvalidParam=InvalidParamValueError, ) - -if __name__ == '__main__': - print("Minimal testing of errors....") - - print "OK" - print diff --git a/secop/protocol/framing/__init__.py b/secop/protocol/framing/__init__.py index a79f531..211ceee 100644 --- a/secop/protocol/framing/__init__.py +++ b/secop/protocol/framing/__init__.py @@ -45,10 +45,10 @@ class Framer(object): raise NotImplemented # now some Implementations -from null import NullFramer -from eol import EOLFramer -from rle import RLEFramer -from demo import DemoFramer +from .null import NullFramer +from .eol import EOLFramer +from .rle import RLEFramer +from .demo import DemoFramer FRAMERS = { 'null': NullFramer, diff --git a/secop/server.py b/secop/server.py index 406eb2a..55dfd58 100644 --- a/secop/server.py +++ b/secop/server.py @@ -22,6 +22,7 @@ # ***************************************************************************** """Define helpers""" import os +import ast import time import psutil import threading @@ -154,9 +155,10 @@ class Server(object): devopts['value'] = devopts.pop('default') # strip '" for k, v in devopts.items(): - for d in ("'", '"'): - if v.startswith(d) and v.endswith(d): - devopts[k] = v[1:-1] + try: + devopts[k] = ast.literal_eval(v) + except Exception: + pass devobj = devclass( self.log.getChild(devname), devopts, devname, self._dispatcher) devs.append([devname, devobj, export]) diff --git a/secop_mlz/amagnet.py b/secop_mlz/amagnet.py new file mode 100644 index 0000000..509a2b8 --- /dev/null +++ b/secop_mlz/amagnet.py @@ -0,0 +1,328 @@ +# -*- 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 +# +# ***************************************************************************** + +""" +Supporting classes for FRM2 magnets, currently only Garfield (amagnet). +""" + +# partially borrowed from nicos + +import math + +from secop.lib import lazy_property, mkthread +from secop.lib.sequence import SequencerMixin, Step +from secop.protocol import status +from secop.datatypes import * +from secop.errors import SECoPServerError, ConfigError, ProgrammingError, CommunicationError, HardwareError, DisabledError +from secop.modules import PARAM, CMD, OVERRIDE, Device, Readable, Driveable + + +class GarfieldMagnet(SequencerMixin, Driveable): + """Garfield Magnet + + uses a polarity switch ('+' or '-') to flip polarity and an onoff switch + to cut power (to be able to switch polarity) in addition to an + unipolar current source. + + B(I) = Ic0 + c1*erf(c2*I) + c3*atan(c4*I) + + Coefficients c0..c4 are given as 'calibration_table' parameter, + the symmetry setting selects which. + """ + + PARAMS = { + 'subdev_currentsource': PARAM('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False), + 'subdev_enable': PARAM('Switch to set for on/off', datatype=StringType(), readonly=True, export=False), + 'subdev_polswitch': PARAM('Switch to set for polarity', datatype=StringType(), readonly=True, export=False), + 'subdev_symmetry': PARAM('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False), + 'userlimits': PARAM('User defined limits of device value', + unit='main', datatype=TupleOf(FloatRange(), FloatRange()), + default=(float('-Inf'), float('+Inf')), readonly=False, poll=10), + 'abslimits': PARAM('Absolute limits of device value', + unit='main', datatype=TupleOf(FloatRange(), FloatRange()), + default=(-0.5, 0.5), poll=True, + ), + 'precision': PARAM('Precision of the device value (allowed deviation ' + 'of stable values from target)', + unit='main', datatype=FloatRange(0.001), default=0.001, readonly=False, + ), + 'ramp': PARAM('Target rate of field change per minute', readonly=False, + unit='main/min', datatype=FloatRange(), default=1.0), + 'calibration': PARAM('Coefficients for calibration ' + 'function: [c0, c1, c2, c3, c4] calculates ' + 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' + ' in T', poll=1, + datatype=ArrayOf(FloatRange(), 5, 5), + default=(1.0, 0.0, 0.0, 0.0, 0.0)), + 'calibrationtable': PARAM('Map of Coefficients for calibration per symmetry setting', + datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5), + short=ArrayOf(FloatRange(), 5, 5), + asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False), + } + + def _current2field(self, current, *coefficients): + """Return field in T for given current in A. + + Should be monotonic and asymetric or _field2current will fail! + + Note: This may be overridden in derived classes. + """ + v = coefficients or self.calibration + if len(v) != 5: + self.log.warning('Wrong number of coefficients in calibration ' + 'data! Need exactly 5 coefficients!') + return current * v[0] + v[1] * math.erf(v[2] * current) + \ + v[3] * math.atan(v[4] * current) + + def _field2current(self, field): + """Return required current in A for requested field in T. + + Default implementation does a binary search using _current2field, + which must be monotonic for this to work! + + Note: This may be overridden in derived classes. + """ + # binary search/bisection + maxcurr = self._currentsource.abslimits[1] + mincurr = -maxcurr + maxfield = self._current2field(maxcurr) + minfield = -maxfield + if not minfield <= field <= maxfield: + raise ValueError(self, + 'requested field %g T out of range %g..%g T' % + (field, minfield, maxfield)) + while minfield <= field <= maxfield: + # binary search + trycurr = 0.5 * (mincurr + maxcurr) + tryfield = self._current2field(trycurr) + if field == tryfield: + self.log.debug('current for %g T is %g A', field, trycurr) + return trycurr # Gotcha! + elif field > tryfield: + # retry upper interval + mincurr = trycurr + minfield = tryfield + else: + # retry lower interval + maxcurr = trycurr + maxfield = tryfield + # if interval is so small, that any error within is acceptable: + if maxfield - minfield < 1e-4: + ratio = (field - minfield) / (maxfield - minfield) + trycurr = (maxcurr - mincurr) * ratio + mincurr + self.log.debug('current for %g T is %g A', field, trycurr) + return trycurr # interpolated + raise ConfigurationError(self, + '_current2field polynome not monotonic!') + + def init(self): + super(GarfieldMagnet, self).init() + self._enable = self.DISPATCHER.get_module(self.subdev_enable) + self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry) + self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch) + self._currentsource = self.DISPATCHER.get_module( + self.subdev_currentsource) + self.init_sequencer(fault_on_error=False, fault_on_stop=False) + self._symmetry.read_value(0) + + def read_calibration(self, maxage=0): + try: + return self.calibrationtable[self._symmetry.value] + except KeyError: + minslope = min(entry[0] + for entry in self.calibrationtable.values()) + self.log.error( + 'unconfigured calibration for symmetry %r' % + self._symmetry.value) + return [minslope, 0, 0, 0, 0] + + def _checkLimits(self, limits): + umin, umax = limits + amin, amax = self.abslimits + if umin > umax: + raise ValueError( + self, 'user minimum (%s) above the user ' + 'maximum (%s)' % (umin, umax)) + if umin < amin - abs(amin * 1e-12): + umin = amin + if umax > amax + abs(amax * 1e-12): + umax = amax + return (umin, umax) + + def write_userlimits(self, value): + limits = self._checkLimits(value) + return limits + + def read_abslimits(self, maxage=0): + maxfield = self._current2field(self._currentsource.abslimits[1]) + # limit to configured value (if any) + maxfield = min(maxfield, max(self.PARAMS['abslimits'].default)) + return -maxfield, maxfield + + def read_ramp(self, maxage=0): + # This is an approximation! + return self.calibration[0] * abs(self._currentsource.ramp) + + def write_ramp(self, newramp): + # This is an approximation! + self._currentsource.ramp = newramp / self.calibration[0] + + def _get_field_polarity(self): + sign = int(self._polswitch.read_value()) + if self._enable.read_value(): + return sign + return 0 + + def _set_field_polarity(self, polarity): + current_pol = self._get_field_polarity() + if current_pol == polarity: + return + if polarity == 0: + return + if current_pol == 0: + # safe to switch + self._polswitch.write_target( + '+1' if polarity == 1 else str(polarity)) + return 0 + if self._currentsource.value < 0.1: + self._polswitch.write_target('0') + return current_pol + # unsafe to switch, go to safe state first + self._currentsource.write_target(0) + + def read_value(self, maxage=0): + return self._current2field( + self._currentsource.read_value(maxage) * + self._get_field_polarity()) + + def read_hw_status(self, maxage=0): + # called from SequencerMixin.read_status if no sequence is running + if self._enable.value == 'Off': + return status.WARN, 'Disabled' + if self._enable.read_status(maxage)[0] != status.OK: + return self._enable.status + if self._polswitch.value in ['0', 0]: + return self._currentsource.status[ + 0], 'Shorted, ' + self._currentsource.status[1] + if self._symmetry.value in ['short', 0]: + return self._currentsource.status[ + 0], 'Shorted, ' + self._currentsource.status[1] + return self._currentsource.read_status(maxage) + + def write_target(self, target): + if target != 0 and self._symmetry.read_value(0) in ['short', 0]: + raise DisabledError( + 'Symmetry is shorted, please select another symmetry first!') + + wanted_current = self._field2current(abs(target)) + wanted_polarity = '-1' if target < 0 else ('+1' if target else '0') + current_polarity = self._get_field_polarity() + + # generate Step sequence and start it + seq = [] + seq.append(Step('preparing', 0, self._prepare_ramp)) + seq.append(Step('recover', 0, self._recover)) + if current_polarity != wanted_polarity: + if self._currentsource.read_value(0) > 0.1: + # switching only allowed if current is low enough -> ramp down + # first + seq.append( + Step( + 'ramping down', + 0.3, + self._ramp_current, + 0, + cleanup=self._ramp_current_cleanup)) + seq.append( + Step( + 'set polarity %s' % + wanted_polarity, + 0.3, + self._set_polarity, + wanted_polarity)) # no cleanup + seq.append( + Step( + 'ramping to %.3fT (%.2fA)' % + (target, + wanted_current), + 0.3, + self._ramp_current, + wanted_current, + cleanup=self._ramp_current_cleanup)) + seq.append(Step('finalize', 0, self._finish_ramp)) + + self.start_sequence(seq) + self.status = 'BUSY', 'ramping' + + # steps for the sequencing + def _prepare_ramp(self, store, *args): + store.old_window = self._currentsource.window + self._currentsource.window = 1 + + def _finish_ramp(self, store, *args): + self._currentsource.window = max(store.old_window, 10) + + def _recover(self, store): + # check for interlock + if self._currentsource.read_status(0)[0] != status.ERROR: + return + # recover from interlock + ramp = self._currentsource.ramp + self._polswitch.write_target('0') # short is safe... + self._polswitch._hw_wait() + self._enable.write_target('On') # else setting ramp won't work + self._enable._hw_wait() + self._currentsource.ramp = 60000 + self._currentsource.target = 0 + self._currentsource.ramp = ramp + # safe state.... if anything of the above fails, the tamperatures may + # be too hot! + + def _ramp_current(self, store, target): + if abs(self._currentsource.value - target) <= 0.05: + # done with this step if no longer BUSY + return self._currentsource.read_status(0)[0] == 'BUSY' + if self._currentsource.status[0] != 'BUSY': + if self._enable.status[0] == 'ERROR': + self._enable.do_reset() + self._enable.read_status(0) + self._enable.write_target('On') + self._enable._hw_wait() + self._currentsource.write_target(target) + return True # repeat + + def _ramp_current_cleanup(self, store, step_was_busy, target): + # don't cleanup if step finished + if step_was_busy: + self._currentsource.write_target(self._currentsource.read_value(0)) + self._currentsource.window = max(store.old_window, 10) + + def _set_polarity(self, store, target): + if self._polswitch.read_status(0)[0] == status.BUSY: + return True + if self._polswitch.value == target: + return False # done with this step + if self._polswitch.read_value(0) != 0: + self._polswitch.write_target(0) + else: + self._polswitch.write_target(target) + return True # repeat diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index b610e8d..565061c 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -164,7 +164,7 @@ class PyTangoDevice(Device): 'tangodevice': PARAM('Tango device name', datatype=StringType(), readonly=True, -# export=True, # for testing only + # export=True, # for testing only export=False, ), } @@ -215,8 +215,8 @@ class PyTangoDevice(Device): def _hw_wait(self): """Wait until hardware status is not BUSY.""" - while PyTangoDevice.doStatus(self, 0)[0] == status.BUSY: - sleep(self._base_loop_delay) + while self.read_status(0)[0] == 'BUSY': + sleep(0.3) def _getProperty(self, name, dev=None): """ @@ -366,8 +366,8 @@ class AnalogInput(PyTangoDevice, Readable): The AnalogInput handles all devices only delivering an analogue value. """ - def init(self): - super(AnalogInput, self).init() + def late_init(self): + super(AnalogInput, self).late_init() # query unit from tango and update value property attrInfo = self._dev.attribute_query('value') # prefer configured unit if nothing is set on the Tango device, else @@ -442,27 +442,15 @@ class AnalogOutput(PyTangoDevice, Driveable): 900), readonly=False, ), - 'pollinterval': PARAM( - '[min, max] sleeptime between polls', - default=[ - 0.5, - 5], - readonly=False, - datatype=TupleOf( - FloatRange( - 0, - 20), - FloatRange( - 0.1, - 120)), - ), - } - OVERRIDES = { - 'value': OVERRIDE(poll=False), } def init(self): - super(AnalogInput, self).init() + super(AnalogOutput, self).init() + # init history + self._history = [] # will keep (timestamp, value) tuple + + def late_init(self): + super(AnalogOutput, self).late_init() # query unit from tango and update value property attrInfo = self._dev.attribute_query('value') # prefer configured unit if nothing is set on the Tango device, else @@ -470,30 +458,14 @@ class AnalogOutput(PyTangoDevice, Driveable): if attrInfo.unit != 'No unit': self.PARAMS['value'].unit = attrInfo.unit - # init history - self._history = [] # will keep (timestamp, value) tuple - mkthread(self._history_thread) - - def _history_thread(self): - while True: - # adaptive sleeping interval - if self.status[0] == status.BUSY: - sleep(min(self.pollinterval)) - else: - sleep(min(max(self.pollinterval) / 2., - max(self.window / 10., min(pollinterval)))) - try: - self.read_value(0) # also append to self._history - # shorten history - while len(self._history) > 2: - # if history would be too short, break - if self._history[-1][0] - \ - self._history[1][0] < self.window: - break - # remove a stale point - self._history.pop(0) - except Exception: - pass + def poll(self, nr): + super(AnalogOutput, self).poll(nr) + while len(self._history) > 2: + # if history would be too short, break + if self._history[-1][0] - self._history[1][0] < self.window: + break + # else: remove a stale point + self._history.pop(0) def read_value(self, maxage=0): value = self._dev.value @@ -560,17 +532,17 @@ class AnalogOutput(PyTangoDevice, Driveable): return self._checkLimits(value) def write_target(self, value=FloatRange()): - try: - self._dev.value = value - except HardwareError: + if self.status[0] == status.BUSY: # changing target value during movement is not allowed by the # Tango base class state machine. If we are moving, stop first. - if self.read_status(0)[0] == status.BUSY: - self.stop() - self._hw_wait() - self._dev.value = value - else: - raise + self.do_stop() + self._hw_wait() + self._dev.value = value + self.read_status(0) # poll our status to keep it updated + + def _hw_wait(self): + while self.read_status(0)[0] == status.BUSY: + sleep(0.3) def do_stop(self): self._dev.Stop() @@ -601,21 +573,21 @@ class Actuator(AnalogOutput): poll=30), } - def read_speed(self): + def read_speed(self, maxage=0): return self._dev.speed def write_speed(self, value): self._dev.speed = value - def read_ramp(self): + def read_ramp(self, maxage=0): return self.read_speed() * 60 def write_ramp(self, value): self.write_speed(value / 60.) - return self.speed * 60 + return self.read_speed(0) * 60 - def do_setposition(self, value): - self._dev.Adjust(value) +# def do_setposition(self, value=FloatRange()): +# self._dev.Adjust(value) class Motor(Actuator): @@ -643,16 +615,16 @@ class Motor(Actuator): unit='main/s^2'), } - def read_refpos(self): + def read_refpos(self, maxage=0): return float(self._getProperty('refpos')) - def read_accel(self): + def read_accel(self, maxage=0): return self._dev.accel def write_accel(self, value): self._dev.accel = value - def read_decel(self): + def read_decel(self, maxage=0): return self._dev.decel def write_decel(self, value): @@ -695,32 +667,32 @@ class TemperatureController(Actuator): 'precision': OVERRIDE(default=0.1), } - def read_ramp(self): + def read_ramp(self, maxage=0): return self._dev.ramp def write_ramp(self, value): self._dev.ramp = value return self._dev.ramp - def read_p(self): + def read_p(self, maxage=0): return self._dev.p def write_p(self, value): self._dev.p = value - def read_i(self): + def read_i(self, maxage=0): return self._dev.i def write_i(self, value): self._dev.i = value - def read_d(self): + def read_d(self, maxage=0): return self._dev.d def write_d(self, value): self._dev.d = value - def read_pid(self): + def read_pid(self, maxage=0): self.read_p() self.read_i() self.read_d() @@ -731,10 +703,10 @@ class TemperatureController(Actuator): self._dev.i = value[1] self._dev.d = value[2] - def read_setpoint(self): + def read_setpoint(self, maxage=0): return self._dev.setpoint - def read_heateroutput(self): + def read_heateroutput(self, maxage=0): return self._dev.heaterOutput @@ -747,21 +719,21 @@ class PowerSupply(Actuator): 'ramp': PARAM('Current/voltage ramp', unit='main/min', datatype=FloatRange(), readonly=False, poll=30,), 'voltage': PARAM('Actual voltage', unit='V', - datatype=FloatRange(), poll=5), + datatype=FloatRange(), poll=-5), 'current': PARAM('Actual current', unit='A', - datatype=FloatRange(), poll=5), + datatype=FloatRange(), poll=-5), } - def read_ramp(self): + def read_ramp(self, maxage=0): return self._dev.ramp def write_ramp(self, value): self._dev.ramp = value - def read_voltage(self): + def read_voltage(self, maxage=0): return self._dev.voltage - def read_current(self): + def read_current(self, maxage=0): return self._dev.current @@ -771,7 +743,7 @@ class DigitalInput(PyTangoDevice, Readable): """ OVERRIDES = { - 'value': OVERRIDE(datatype=IntRange(0)), + 'value': OVERRIDE(datatype=IntRange()), } def read_value(self, maxage=0): @@ -835,8 +807,8 @@ class DigitalOutput(PyTangoDevice, Driveable): """ OVERRIDES = { - 'value': OVERRIDE(datatype=IntRange(0)), - 'target': OVERRIDE(datatype=IntRange(0)), + 'value': OVERRIDE(datatype=IntRange()), + 'target': OVERRIDE(datatype=IntRange()), } def read_value(self, maxage=0): @@ -856,17 +828,23 @@ class NamedDigitalOutput(DigitalOutput): A DigitalOutput with numeric values mapped to names. """ - PARAMS = { - 'mapping': PARAM('A dictionary mapping state names to integers', - datatype=StringType(), export=False), # XXX: !!! - } +# PARAMS = { +# 'mapping': PARAM('A dictionary mapping state names to integers', +# datatype=EnumType(), export=False), # XXX: !!! +# } +# +# def init(self): +# super(NamedDigitalOutput, self).init() +# try: # XXX: !!! +# self.PARAMS['value'].datatype = EnumType(**eval(self.mapping)) +# except Exception as e: +# raise ValueError('Illegal Value for mapping: %r' % e) - def init(self): - super(NamedDigitalOutput, self).init() - try: # XXX: !!! - self.PARAMS['value'].datatype = EnumType(**eval(self.mapping)) - except Exception as e: - raise ValueError('Illegal Value for mapping: %r' % e) + def write_target(self, target): + # map from enum-str to integer value + self._dev.value = self.PARAMS[ + 'target'].datatype.reversed.get(target, target) + self.read_value() class PartialDigitalOutput(NamedDigitalOutput): @@ -930,19 +908,19 @@ class StringIO(PyTangoDevice, Device): group='communication'), } - def read_bustimeout(self): + def read_bustimeout(self, maxage=0): return self._dev.communicationTimeout def write_bustimeout(self, value): self._dev.communicationTimeout = value - def read_endofline(self): + def read_endofline(self, maxage=0): return self._dev.endOfLine def write_endofline(self, value): self._dev.endOfLine = value - def read_startofline(self): + def read_startofline(self, maxage=0): return self._dev.startOfLine def write_startofline(self, value):