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:
parent
63418fce04
commit
2bb96bea70
@ -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
103
etc/amagnet.cfg
Normal 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
|
@ -1,4 +1,5 @@
|
||||
#--extra-index-url https://forge.frm2.tum.de/simple
|
||||
serial
|
||||
mlzlog >=0.2.0
|
||||
# for generating docu
|
||||
markdown>=2.6
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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]))
|
||||
|
@ -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])
|
||||
|
182
secop/gui/params/__init__.py
Normal file
182
secop/gui/params/__init__.py
Normal 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)
|
60
secop/gui/ui/cmdbuttons.ui
Normal file
60
secop/gui/ui/cmdbuttons.ui
Normal 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>
|
@ -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">
|
||||
|
79
secop/gui/ui/modulebuttons.ui
Normal file
79
secop/gui/ui/modulebuttons.ui
Normal 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>
|
@ -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/>
|
||||
|
@ -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>>>></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<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><!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></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</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></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>>>></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>
|
||||
|
@ -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/>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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 -----")
|
||||
|
@ -91,9 +91,3 @@ EXCEPTIONS = dict(
|
||||
Readonly=ReadonlyError,
|
||||
CommandFailed=CommandFailedError,
|
||||
InvalidParam=InvalidParamValueError, )
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Minimal testing of errors....")
|
||||
|
||||
print "OK"
|
||||
print
|
||||
|
@ -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,
|
||||
|
@ -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
328
secop_mlz/amagnet.py
Normal 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
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user