provide setup for MLZ_Amagnet to be used @PSI soon

Also implement lots of fixes and improvements.

fixes: #3381

Change-Id: Ibe6664da00756ae5813b90f190295045808b2ff0
This commit is contained in:
Enrico Faulhaber 2017-07-20 16:29:21 +02:00
parent 63418fce04
commit 2bb96bea70
31 changed files with 1510 additions and 403 deletions

View File

@ -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')

103
etc/amagnet.cfg Normal file
View File

@ -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

View File

@ -1,4 +1,5 @@
#--extra-index-url https://forge.frm2.tum.de/simple
serial
mlzlog >=0.2.0
# for generating docu
markdown>=2.6

View File

@ -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)

View File

@ -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 = '<unset>'
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]

View File

@ -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,
)

View File

@ -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'

View File

@ -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:

View File

@ -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]))

View File

@ -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, '<not supported>')))
# 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])

View File

@ -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 <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
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)

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>730</width>
<height>33</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>6</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Arguments:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="cmdLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>256</width>
<height>0</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="cmdPushButton">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -29,6 +29,9 @@
</property>
<item>
<widget class="QComboBox" name="visibilityComboBox">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>user</string>
@ -61,6 +64,9 @@
</item>
<item>
<widget class="QCheckBox" name="validateCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Validate locally</string>
</property>
@ -104,7 +110,7 @@
<x>0</x>
<y>0</y>
<width>1228</width>
<height>23</height>
<height>33</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>748</width>
<height>74</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QLineEdit" name="currentLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>256</width>
<height>0</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="targetLineEdit"/>
</item>
<item>
<widget class="QComboBox" name="targetComboBox"/>
</item>
</layout>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="cmdPushButton">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLineEdit" name="statusLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -6,53 +6,15 @@
<rect>
<x>0</x>
<y>0</y>
<width>230</width>
<height>195</height>
<width>257</width>
<height>162</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Module name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="moduleNameLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<item row="4" column="0">
<widget class="QGroupBox" name="paramGroupBox">
<property name="title">
<string>Parameters:</string>
@ -76,44 +38,7 @@
</layout>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="propertyGroupBox">
<property name="title">
<string>Properties:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
</layout>
</widget>
</item>
<item row="5" column="0">
<item row="6" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -126,6 +51,54 @@
</property>
</spacer>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="propertyGroupBox">
<property name="title">
<string>Properties:</string>
</property>
<layout class="QGridLayout" name="gridLayout_3"/>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Module name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="moduleNameLabel">
<property name="font">
<font>
<pointsize>18</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="commandGroupBox">
<property name="title">
<string>Commands:</string>
</property>
<layout class="QGridLayout" name="gridLayout_5"/>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -85,49 +85,91 @@
</layout>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QLineEdit" name="msgLineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&gt;&gt;&gt;</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="sendPushButton">
<property name="text">
<string>Send</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="clearPushButton">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QTextBrowser" name="logTextBrowser">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="consoleTab">
<attribute name="title">
<string>Console</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTextBrowser" name="logTextBrowser">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:12pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-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;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QPushButton" name="clearPushButton">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="msgLineEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&gt;&gt;&gt;</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="sendPushButton">
<property name="text">
<string>Send</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="modulesTab">
<property name="enabled">
<bool>true</bool>
</property>
<attribute name="title">
<string>Modules</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>610</width>
<height>324</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_5"/>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>730</width>
<height>33</height>
<height>39</height>
</rect>
</property>
<property name="windowTitle">
@ -36,25 +36,15 @@
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Current: </string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Set: </string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="targetValueComboBox"/>
</item>
<item row="0" column="4">
<widget class="QComboBox" name="comboBox_2"/>
<widget class="QComboBox" name="setComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="0" column="5">
<spacer name="horizontalSpacer_2">
@ -69,6 +59,30 @@
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="currentLabel">
<property name="text">
<string>Current: </string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="setLabel">
<property name="text">
<string>Set: </string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="currentLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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):

View File

@ -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='<module>',
@ -289,17 +291,17 @@ class DemoEncoder(MessageEncoder):
nonce='<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 -----")

View File

@ -91,9 +91,3 @@ EXCEPTIONS = dict(
Readonly=ReadonlyError,
CommandFailed=CommandFailedError,
InvalidParam=InvalidParamValueError, )
if __name__ == '__main__':
print("Minimal testing of errors....")
print "OK"
print

View File

@ -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,

View File

@ -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])

328
secop_mlz/amagnet.py Normal file
View File

@ -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 <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""
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

View File

@ -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):