Bug hunting and polishing

Change-Id: I0f05730dd4e01e926ab0c4870c27ed5754f3ccfd
This commit is contained in:
Enrico Faulhaber 2017-01-20 18:21:27 +01:00
parent 8e3d0da5dd
commit d5e935788f
18 changed files with 552 additions and 202 deletions

View File

@ -38,7 +38,10 @@ def main(argv=None):
if argv is None:
argv = sys.argv
loggers.initLogging('gui', 'debug')
if '-d' in argv:
loggers.initLogging('gui', 'debug')
else:
loggers.initLogging('gui', 'info')
app = QApplication(argv)

View File

@ -1,8 +1,12 @@
[server]
[equipment]
id=demonstration
[interface testing]
interface=tcp
bindto=0.0.0.0
bindport=10767
interface = tcp
framing=demo
# protocol to use for this interface
framing=eol
encoding=demo
[device heatswitch]

View File

@ -35,6 +35,7 @@ from secop.lib.parsing import parse_time, format_time
from secop.protocol.encoding import ENCODERS
from secop.protocol.framing import FRAMERS
from secop.protocol.messages import *
from secop.protocol.errors import EXCEPTIONS
class TCPConnection(object):
@ -115,9 +116,9 @@ class Value(object):
def __init__(self, value, qualifiers={}):
self.value = value
if 't' in qualifiers:
self.t = parse_time(qualifiers.pop('t'))
self.__dict__.update(qualifiers)
if 't' in qualifiers:
self.t = parse_time(qualifiers['t'])
def __repr__(self):
r = []
@ -153,9 +154,12 @@ class Client(object):
port = int(opts.pop('port', 10767))
self.contactPoint = "tcp://%s:%d" % (host, port)
self.connection = TCPConnection(host, port)
# maps an expected reply to an list containing a single Event()
# upon rcv of that reply, the event is set and the listitem 0 is
# appended with the reply-tuple
# maps an expected reply to a list containing a single Event()
# upon rcv of that reply, entry is appended with False and
# the data of the reply.
# if an error is received, the entry is appended with True and an
# appropriate Exception.
# Then the Event is set.
self.expected_replies = {}
# maps spec to a set of callback functions (or single_shot callbacks)
@ -193,47 +197,60 @@ class Client(object):
self.log.info('connected to: ' + line.strip())
self.secop_id = line
continue
msgtype, spec, data = self._decode_message(line)
msgtype, spec, data = self.decode_message(line)
if msgtype in ('update', 'changed'):
# handle async stuff
self._handle_event(spec, data)
if msgtype != 'update':
# handle sync stuff
if msgtype in self.expected_replies:
entry = self.expected_replies[msgtype]
entry.extend([msgtype, spec, data])
# wake up calling process
entry[0].set()
elif msgtype == "error":
# XXX: hack!
if len(self.expected_replies) == 1:
entry = self.expected_replies.values()[0]
entry.extend([msgtype, spec, data])
# wake up calling process
entry[0].set()
else: # try to find the right request....
print data[0] # should be the origin request
# XXX: make an assignment of ERROR to an expected reply.
self.log.error('TODO: handle ERROR replies!')
else:
self.log.error('ignoring unexpected reply %r' % line)
# handle sync stuff
self._handle_sync_reply(msgtype, spec, data)
def _encode_message(self, requesttype, spec='', data=Ellipsis):
def _handle_sync_reply(self, msgtype, spec, data):
# handle sync stuff
if msgtype == "error":
# find originating msgtype and map to expected_reply_type
# errormessages carry to offending request as the first
# result in the resultist
_msgtype, _spec, _data = self.decode_message(data[0])
_reply = self._get_reply_from_request(_msgtype)
entry = self.expected_replies.get((_reply, _spec), None)
if entry:
self.log.error("request %r resulted in Error %r" %
(data[0], spec))
entry.extend([True, EXCEPTIONS[spec](data)])
entry[0].set()
return
self.log.error("got an unexpected error %s %r" %
(spec, data[0]))
return
if msgtype == "describing":
data = [spec, data]
spec = ''
entry = self.expected_replies.get((msgtype, spec), None)
if entry:
self.log.debug("got expected reply '%s %s'" %
(msgtype, spec) if spec else
"got expected reply '%s'" % msgtype)
entry.extend([False, data])
entry[0].set()
return
def encode_message(self, requesttype, spec='', data=None):
"""encodes the given message to a string
"""
req = [str(requesttype)]
if spec:
req.append(str(spec))
if data is not Ellipsis:
if data is not None:
req.append(json.dumps(data))
req = ' '.join(req)
return req
def _decode_message(self, msg):
def decode_message(self, msg):
"""return a decoded message tripel"""
msg = msg.strip()
if ' ' not in msg:
return msg, None, None
return msg, '', None
msgtype, spec = msg.split(' ', 1)
data = None
if ' ' in spec:
@ -277,8 +294,7 @@ class Client(object):
return self._getDescribingModuleData(module)['parameters'][parameter]
def _issueDescribe(self):
_, self.equipment_id, self.describing_data = self.communicate(
'describe')
self.equipment_id, self.describing_data = self.communicate('describe')
for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items():
@ -299,53 +315,71 @@ class Client(object):
self.callbacks.setdefault('%s:%s' %
(module, parameter), set()).discard(cb)
def communicate(self, msgtype, spec='', data=Ellipsis):
def _get_reply_from_request(self, requesttype):
# maps each (sync) request to the corresponding reply
# XXX: should go to the encoder! and be imported here (or make a
# translating method)
# XXX: should go to the encoder! and be imported here
REPLYMAP = {
"describe": "describing",
"do": "done",
"change": "changed",
"activate": "active",
"deactivate": "inactive",
"*IDN?": "SECoP,",
"read": "update",
#"*IDN?": "SECoP,", # XXX: !!!
"ping": "pong",
}
return REPLYMAP.get(requesttype, requesttype)
def communicate(self, msgtype, spec='', data=None):
self.log.debug('communicate: %r %r %r' % (msgtype, spec, data))
if self.stopflag:
raise RuntimeError('alreading stopping!')
if msgtype == 'read':
# send a poll request and then check incoming events
if ':' not in spec:
spec = spec + ':value'
event = threading.Event()
result = ['update', spec]
self.single_shots.setdefault(spec, set()).add(
lambda d: (result.append(d), event.set()))
self.connection.writeline(
self._encode_message(
msgtype, spec, data))
if event.wait(10):
return tuple(result)
raise RuntimeError("timeout upon waiting for reply!")
if msgtype == "*IDN?":
return self.secop_id
rply = REPLYMAP[msgtype]
if rply in self.expected_replies:
if msgtype not in ('*IDN?', 'describe', 'activate',
'deactivate', 'do', 'change', 'read', 'ping', 'help'):
raise EXCEPTIONS['Protocol'](errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' %
msgtype,
origin=self.encode_message(msgtype, spec, data))
# sanitize input + handle syntactic sugar
msgtype = str(msgtype)
spec = str(spec)
if msgtype == 'change' and ':' not in spec:
spec = spec + ':target'
if msgtype == 'read' and ':' not in spec:
spec = spec + ':value'
# check if a such a request is already out
rply = self._get_reply_from_request(msgtype)
if (rply, spec) in self.expected_replies:
raise RuntimeError(
"can not have more than one requests of the same type at the same time!")
# prepare sending request
event = threading.Event()
self.expected_replies[rply] = [event]
self.expected_replies[(rply, spec)] = [event]
self.log.debug('prepared reception of %r msg' % rply)
self.connection.writeline(self._encode_message(msgtype, spec, data))
self.log.debug('sent %r msg' % msgtype)
if event.wait(10): # wait 10s for reply
# send request
msg = self.encode_message(msgtype, spec, data)
self.connection.writeline(msg)
self.log.debug('sent msg %r' % msg)
# wait for reply. timeout after 10s
if event.wait(10):
self.log.debug('checking reply')
result = self.expected_replies[rply][1:4]
del self.expected_replies[rply]
# if result[0] == "ERROR":
# raise RuntimeError('Got %s! %r' % (str(result[1]), repr(result[2])))
event, is_error, result = self.expected_replies.pop((rply, spec))
if is_error:
# if error, result contains the rigth Exception to raise
raise result
return result
del self.expected_replies[rply]
# timed out
del self.expected_replies[(rply, spec)]
# XXX: raise a TimedOut ?
raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype)
def quit(self):
@ -379,6 +413,9 @@ class Client(object):
return result
def getParameter(self, module, parameter):
return self.communicate('read', '%s:%s' % (module, parameter))
def setParameter(self, module, parameter, value):
validator = self._getDescribingParameterData(module,
parameter)['validator']
@ -417,7 +454,11 @@ class Client(object):
def getProperties(self, module, parameter):
return self.describing_data['modules'][
module]['parameters'][parameter].items()
module]['parameters'][parameter]
def syncCommunicate(self, *msg):
return self.communicate(*msg)
def ping(self, pingctr=[0]):
pingctr[0] = pingctr[0] + 1
self.communicate("ping", pingctr[0])

View File

@ -35,7 +35,7 @@ import threading
from secop.lib.parsing import format_time
from secop.errors import ConfigError, ProgrammingError
from secop.protocol import status
from secop.validators import enum, vector, floatrange
from secop.validators import enum, vector, floatrange, validator_to_str
EVENT_ONLY_ON_CHANGED_VALUES = False
@ -74,9 +74,9 @@ class PARAM(object):
unit=self.unit,
readonly=self.readonly,
value=self.value,
timestamp=format_time(self.timestamp) if self.timestamp else None,
validator=str(self.validator) if not isinstance(
self.validator, type) else self.validator.__name__
timestamp=format_time(
self.timestamp) if self.timestamp else None,
validator=validator_to_str(self.validator),
)
@ -260,7 +260,7 @@ class Device(object):
# only check if validator given
try:
v = validator(v)
except ValueError as e:
except (ValueError, TypeError) as e:
raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e))
setattr(self, k, v)
@ -285,22 +285,22 @@ class Readable(Device):
default="Readable", validator=str),
'value': PARAM('current value of the device', readonly=True, default=0.),
'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1, 120),),
'status': PARAM('current status of the device', default=status.OK,
validator=enum(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}),
# 'status': PARAM('current status of the device', default=status.OK,
# validator=enum(**{'idle': status.OK,
# 'BUSY': status.BUSY,
# 'WARN': status.WARN,
# 'UNSTABLE': status.UNSTABLE,
# 'ERROR': status.ERROR,
# 'UNKNOWN': status.UNKNOWN}),
# readonly=True),
'status': PARAM('current status of the device', default=(status.OK, ''),
validator=vector(enum(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}), str),
readonly=True),
'status2': PARAM('current status of the device', default=(status.OK, ''),
validator=vector(enum(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}), str),
readonly=True),
}
def init(self):

View File

@ -68,21 +68,27 @@ class Switch(Driveable):
def read_status(self, maxage=0):
self.log.info("read status")
self._update()
info = self._update()
if self.target == self.value:
return status.OK
return status.BUSY
return status.OK, ''
return status.BUSY, info
def _update(self):
started = self.PARAMS['target'].timestamp
info = ''
if self.target > self.value:
info = 'waiting for ON'
if time.time() > started + self.switch_on_time:
self.log.debug('is switched ON')
info = 'is switched ON'
self.value = self.target
elif self.target < self.value:
info = 'waiting for OFF'
if time.time() > started + self.switch_off_time:
self.log.debug('is switched OFF')
info = 'is switched OFF'
self.value = self.target
if info:
self.log.debug(info)
return info
class MagneticField(Driveable):
@ -101,7 +107,7 @@ class MagneticField(Driveable):
def init(self):
self._state = 'idle'
self._heatswitch = self.DISPATCHER.get_device(self.heatswitch)
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()
@ -116,7 +122,8 @@ class MagneticField(Driveable):
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
return status.OK if self._state == 'idle' else status.BUSY
return (status.OK, '') if self._state == 'idle' else (
status.BUSY, self._state)
def _thread(self):
loopdelay = 1
@ -202,9 +209,9 @@ class SampleTemp(Driveable):
ts = time.time()
if self.value == self.target:
if self.status != status.OK:
self.status = status.OK
self.status = status.OK, ''
else:
self.status = status.BUSY
self.status = status.BUSY, 'ramping'
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
@ -230,14 +237,14 @@ class Label(Readable):
def read_value(self, maxage=0):
strings = [self.system]
dev_ts = self.DISPATCHER.get_device(self.subdev_ts)
dev_ts = self.DISPATCHER.get_module(self.subdev_ts)
if dev_ts:
strings.append('at %.3f %s' %
(dev_ts.read_value(), dev_ts.PARAMS['value'].unit))
else:
strings.append('No connection to sample temp!')
dev_mf = self.DISPATCHER.get_device(self.subdev_mf)
dev_mf = self.DISPATCHER.get_module(self.subdev_mf)
if dev_mf:
mf_stat = dev_mf.read_status()
mf_mode = dev_mf.mode
@ -262,8 +269,8 @@ class ValidatorTest(Readable):
'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1),
'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')),
'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]),
'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0),
'positive': PARAM('positive', validator=positive(), readonly=False, default=1),
'nonnegative': PARAM('nonnegative', validator=nonnegative, readonly=False, default=0),
'positive': PARAM('positive', validator=positive, readonly=False, default=1),
'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4),
'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,),
}

View File

@ -24,7 +24,7 @@
import random
from secop.devices.core import Readable, Driveable, PARAM
from secop.validators import floatrange
from secop.validators import floatrange, positive
class LN2(Readable):
@ -65,6 +65,8 @@ class Temp(Driveable):
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
'target': PARAM("Target temperature", default=300.0,
validator=positive, readonly=False, unit='K'),
}
def read_value(self, maxage=0):

View File

@ -27,6 +27,7 @@ from PyQt4.QtCore import pyqtSignature as qtsig, QObject, pyqtSignal
from secop.gui.util import loadUi
from secop.gui.nodectrl import NodeCtrl
from secop.gui.modulectrl import ModuleCtrl
from secop.gui.paramview import ParameterView
from secop.client.baseclient import Client as SECNode
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
@ -35,7 +36,7 @@ ITEM_TYPE_PARAMETER = QTreeWidgetItem.UserType + 3
class QSECNode(SECNode, QObject):
newData = pyqtSignal(str, str, object) # module, parameter, data
newData = pyqtSignal(str, str, object) # module, parameter, data
def __init__(self, opts, autoconnect=False, parent=None):
SECNode.__init__(self, opts, autoconnect)
@ -79,7 +80,7 @@ class MainWindow(QMainWindow):
@qtsig('')
def on_actionAdd_SEC_node_triggered(self):
host, ok = QInputDialog.getText(self, 'Add SEC node',
'Enter SEC node hostname:')
'Enter SEC node hostname:')
if not ok:
return
@ -91,6 +92,10 @@ class MainWindow(QMainWindow):
self._displayNode(current.text(0))
elif current.type() == ITEM_TYPE_MODULE:
self._displayModule(current.parent().text(0), current.text(0))
elif current.type() == ITEM_TYPE_PARAMETER:
self._displayParameter(current.parent().parent().text(0),
current.parent().text(0),
current.text(0))
def _addNode(self, host):
@ -99,9 +104,10 @@ class MainWindow(QMainWindow):
if ':' in host:
host, port = host.split(':', 1)
port = int(port)
node = QSECNode({'connectto':host, 'port':port}, parent=self)
node = QSECNode({'connectto': host, 'port': port}, parent=self)
host = '%s:%d' % (host, port)
host = '%s (%s)' % (node.equipment_id, host)
self._nodes[host] = node
# fill tree
@ -127,6 +133,13 @@ class MainWindow(QMainWindow):
def _displayModule(self, node, module):
self._replaceCtrlWidget(ModuleCtrl(self._nodes[node], module))
def _displayParameter(self, node, module, parameter):
self._replaceCtrlWidget(
ParameterView(
self._nodes[node],
module,
parameter))
def _replaceCtrlWidget(self, new):
old = self.splitter.widget(1).layout().takeAt(0)
if old:

View File

@ -26,10 +26,12 @@ 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, setpoint
def __init__(self, module, parameter, initval='', parent=None):
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')
@ -37,20 +39,24 @@ class ParameterButtons(QWidget):
self._parameter = parameter
self.currentLineEdit.setText(str(initval))
if readonly:
self.setPushButton.setEnabled(False)
self.setLineEdit.setEnabled(False)
def on_setPushButton_clicked(self):
self.setRequested.emit(self._module, self._parameter,
self.setLineEdit.text())
self.setLineEdit.text())
class ModuleCtrl(QWidget):
def __init__(self, node, module, parent=None):
super(ModuleCtrl, self).__init__(parent)
loadUi(self, 'modulectrl.ui')
self._node = node
self._module = module
self._paramWidgets = {} # widget cache do avoid garbage collection
self._paramWidgets = {} # widget cache do avoid garbage collection
self.moduleNameLabel.setText(module)
self._initModuleWidgets()
@ -68,8 +74,12 @@ class ModuleCtrl(QWidget):
label = QLabel(param + ':')
label.setFont(font)
props = self._node.getProperties(self._module, param)
buttons = ParameterButtons(self._module, param,
initValues[param].value)
initValues[param].value,
props['readonly'])
buttons.setRequested.connect(self._node.setParameter)
self.paramGroupBox.layout().addWidget(label, row, 0)

View File

@ -27,8 +27,11 @@ from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics
from PyQt4.QtCore import pyqtSignature as qtsig, Qt
from secop.gui.util import loadUi
from secop.protocol.errors import SECOPError
class NodeCtrl(QWidget):
def __init__(self, node, parent=None):
super(NodeCtrl, self).__init__(parent)
loadUi(self, 'nodectrl.ui')
@ -49,9 +52,12 @@ class NodeCtrl(QWidget):
self._addLogEntry('<span style="font-weight:bold">Request:</span> '
'%s:' % msg, raw=True)
msg = msg.split(' ', 2)
reply = self._node.syncCommunicate(*msg)
self._addLogEntry(reply, newline=True, pretty=True)
# msg = msg.split(' ', 2)
try:
reply = self._node.syncCommunicate(*self._node.decode_message(msg))
self._addLogEntry(reply, newline=True, pretty=True)
except SECOPError as e:
self._addLogEntry(e, newline=True, pretty=True, error=True)
@qtsig('')
def on_clearPushButton_clicked(self):
@ -64,13 +70,19 @@ class NodeCtrl(QWidget):
self._addLogEntry('=========================')
self._addLogEntry('', newline=True)
def _addLogEntry(self, msg, newline=False, pretty=False, raw=False):
def _addLogEntry(self, msg, newline=False,
pretty=False, raw=False, error=False):
if pretty:
msg = pprint.pformat(msg, width=self._getLogWidth())
if not raw:
msg = '<pre>%s</pre>' % Qt.escape(str(msg)).replace('\n', '<br />')
if error:
msg = '<div style="color:#FF0000"><b><pre>%s</pre></b></div>' % Qt.escape(
str(msg)).replace('\n', '<br />')
else:
msg = '<pre>%s</pre>' % Qt.escape(str(msg)
).replace('\n', '<br />')
content = ''
if self.logTextBrowser.toPlainText():
@ -89,4 +101,3 @@ class NodeCtrl(QWidget):
# due to monospace)
result = self.logTextBrowser.width() / fontMetrics.width('a')
return result

81
secop/gui/paramview.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2017 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, QSizePolicy
from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal
from secop.gui.util import loadUi
from secop.validators import validator_to_str
class ParameterView(QWidget):
def __init__(self, node, module, parameter, parent=None):
super(ParameterView, self).__init__(parent)
loadUi(self, 'paramview.ui')
self._node = node
self._module = module
self._parameter = parameter
self._propWidgets = {} # widget cache do avoid garbage collection
self.paramNameLabel.setText("%s:%s" % (module, parameter))
self._initParamWidgets()
# self._node.newData.connect(self._updateValue)
def _initParamWidgets(self):
# initValues = self._node.queryCache(self._module) #? mix live data?
row = 0
font = self.font()
font.setBold(True)
props = self._node._getDescribingParameterData(
self._module, self._parameter)
for prop in sorted(props):
label = QLabel(prop + ':')
label.setFont(font)
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
# make 'display' label
if prop == 'validator':
view = QLabel(validator_to_str(props[prop]))
else:
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)
self._propWidgets[prop] = (label, view)
row += 1
def _updateValue(self, module, parameter, value):
if module != self._module:
return
self._paramWidgets[parameter][1].currentLineEdit.setText(str(value[0]))

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>730</width>
<height>31</height>
<height>33</height>
</rect>
</property>
<property name="windowTitle">
@ -33,7 +33,7 @@
<item row="0" column="1">
<widget class="QLineEdit" name="currentLineEdit">
<property name="enabled">
<bool>false</bool>
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
@ -41,6 +41,9 @@
<height>0</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">

99
secop/gui/ui/paramview.ui Normal file
View File

@ -0,0 +1,99 @@
<?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>230</width>
<height>121</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>Parameter name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="paramNameLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="propertyGroupBox">
<property name="title">
<string>Parameters:</string>
</property>
<layout class="QGridLayout" name="gridLayout_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>
<property name="spacing">
<number>6</number>
</property>
</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>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -45,6 +45,7 @@ from messages import *
from errors import *
from secop.lib.parsing import format_time
class Dispatcher(object):
def __init__(self, logger, options):
@ -205,7 +206,7 @@ class Dispatcher(object):
def get_descriptive_data(self):
# XXX: be lazy and cache this?
result = {'modules':{}}
result = {'modules': {}}
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
@ -335,9 +336,9 @@ class Dispatcher(object):
res = self._setParamValue(msg.module, 'target', msg.value)
res.parameter = 'target'
# self.broadcast_event(res)
if conn in self._active_connections:
return None # already send to myself
return res # send reply to inactive conns
# if conn in self._active_connections:
# return None # already send to myself
return res
def handle_Command(self, conn, msg):
# notify all by sending CommandReply

View File

@ -27,7 +27,7 @@
from secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError
from secop.protocol.errors import ProtocolError
import ast
import re
@ -389,7 +389,7 @@ class DemoEncoder_MZ(MessageEncoder):
# errors
ErrorReply: lambda msg: "",
InternalError: lambda msg: "",
ProtocollError: lambda msg: "",
ProtocolError: lambda msg: "",
CommandFailedError: lambda msg: "error CommandError %s:%s %s" % (msg.device, msg.param, msg.error),
NoSuchCommandError: lambda msg: "error NoSuchCommand %s:%s" % (msg.device, msg.param, msg.error),
NoSuchDeviceError: lambda msg: "error NoSuchModule %s" % msg.device,

View File

@ -28,7 +28,7 @@
from secop.lib.parsing import format_time
from secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError
#from secop.protocol.errors import ProtocolError
import ast
import re
@ -71,7 +71,7 @@ HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text
ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand',
'CommandFailed', 'ReadOnly', 'BadValue', 'CommunicationFailed',
'IsBusy', 'IsError', 'SyntaxError', 'InternalError',
'IsBusy', 'IsError', 'ProtocolError', 'InternalError',
'CommandRunning', 'Disabled', ]
# note: above strings need to be unique in the sense, that none is/or
# starts with another
@ -83,15 +83,18 @@ def encode_cmd_result(msgobj):
q['t'] = format_time(q['t'])
return msgobj.result, q
def encode_value_data(vobj):
q = vobj.qualifiers.copy()
if 't' in q:
q['t'] = format_time(q['t'])
return vobj.value, q
def encode_error_msg(emsg):
# note: result is JSON-ified....
return [emsg.origin, dict( (k,getattr(emsg, k)) for k in emsg.ARGS if k != 'origin')]
return [emsg.origin, dict((k, getattr(emsg, k))
for k in emsg.ARGS if k != 'origin')]
class DemoEncoder(MessageEncoder):
@ -163,6 +166,14 @@ class DemoEncoder(MessageEncoder):
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
return '\n'.join('%s %d %s' % (HELPREPLY, i + 1, l.strip())
for i, l in enumerate(text.split('\n')[:-1]))
if isinstance(msg, HeartbeatRequest):
if msg.nonce:
return 'ping %s' % msg.nonce
return 'ping'
if isinstance(msg, HeartbeatReply):
if msg.nonce:
return 'pong %s' % msg.nonce
return 'pong'
for msgcls, parts in self.ENCODEMAP.items():
if isinstance(msg, msgcls):
# resolve lambdas
@ -183,12 +194,12 @@ class DemoEncoder(MessageEncoder):
return IdentifyReply(version_string=encoded)
return HelpMessage()
return ErrorMessage(errorclass='SyntaxError',
errorinfo='Regex did not match!',
is_request=True)
# return ErrorMessage(errorclass='Protocol',
# errorinfo='Regex did not match!',
# is_request=True)
msgtype, msgspec, data = match.groups()
if msgspec is None and data:
return ErrorMessage(errorclass='InternalError',
return ErrorMessage(errorclass='Internal',
errorinfo='Regex matched json, but not spec!',
is_request=True,
origin=encoded)
@ -206,10 +217,10 @@ class DemoEncoder(MessageEncoder):
errorinfo=[repr(err), str(encoded)],
origin=encoded)
msg = self.DECODEMAP[msgtype](msgspec, data)
msg.setvalue("origin",encoded)
msg.setvalue("origin", encoded)
return msg
return ErrorMessage(
errorclass='SyntaxError',
errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' %
encoded,
is_request=True,

View File

@ -30,12 +30,26 @@ class SECOPError(RuntimeError):
for k, v in kwds.items():
setattr(self, k, v)
def __repr__(self):
args = ', '.join(map(repr, self.args))
kwds = ', '.join(['%s=%r' % i for i in self.__dict__.items()])
res = []
if args:
res.append(args)
if kwds:
res.append(kwds)
return '%s(%s)' % (self.name, ', '.join(res))
@property
def name(self):
return self.__class__.__name__[:-len('Error')]
class InternalError(SECOPError):
pass
class ProtocollError(SECOPError):
class ProtocolError(SECOPError):
pass
@ -56,6 +70,10 @@ class ReadonlyError(SECOPError):
pass
class BadValueError(SECOPError):
pass
class CommandFailedError(SECOPError):
pass
@ -64,6 +82,18 @@ class InvalidParamValueError(SECOPError):
pass
EXCEPTIONS = dict(
Internal=InternalError,
Protocol=ProtocolError,
NoSuchModule=NoSuchModuleError,
NoSuchParam=NoSuchParamError,
NoSuchCommand=NoSuchCommandError,
BadValue=BadValueError,
Readonly=ReadonlyError,
CommandFailed=CommandFailedError,
InvalidParam=InvalidParamValueError,
)
if __name__ == '__main__':
print("Minimal testing of errors....")

View File

@ -69,11 +69,12 @@ class Value(object):
devspec = '%s:%s()' % (devspec, self.command)
return '%s:Value(%s)' % (devspec, ', '.join(
[repr(self.value)] +
['%s=%s' % (k, format_time(v) if k=="timestamp" else repr(v)) for k, v in self.qualifiers.items()]))
['%s=%s' % (k, format_time(v) if k == "timestamp" else repr(v)) for k, v in self.qualifiers.items()]))
class Request(Message):
is_request = True
def get_reply(self):
"""returns a Reply object prefilled with the attributes from this request."""
m = Message()
@ -93,12 +94,12 @@ class Request(Message):
m.origin = self.origin
for k in self.ARGS:
m.setvalue(k, self.__dict__[k])
m.setvalue("errorclass", errorclass[:-5]
if errorclass.endswith('rror')
else errorclass)
m.setvalue("errorclass", errorclass[:-5]
if errorclass.endswith('rror')
else errorclass)
m.setvalue("errorinfo", errorinfo)
return m
class IdentifyRequest(Request):
pass
@ -190,6 +191,4 @@ class ErrorMessage(Message):
class HelpMessage(Request):
is_reply = True #!sic!
is_reply = True # !sic!

View File

@ -30,15 +30,14 @@
# if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...)
# to get the internal representation (for the code), call method convert
class ProgrammingError(Exception):
pass
from errors import ProgrammingError
class Validator(object):
# list of tuples: (name, converter)
params = []
valuetype = float
argstr = ''
def __init__(self, *args, **kwds):
plist = self.params[:]
@ -49,7 +48,7 @@ class Validator(object):
for pval in args:
pname, pconv = plist.pop(0)
if pname in kwds:
raise ProgrammingError('%s: positional parameter %s als given '
raise ProgrammingError('%s: positional parameter %s is given '
'as keyword!' % (
self.__class__.__name__,
pname))
@ -67,18 +66,23 @@ class Validator(object):
raise ProgrammingError('%s got unknown arguments: %s' % (
self.__class__.__name__,
', '.join(list(kwds.keys()))))
def __repr__(self):
params = ['%s=%r' % (pn[0], self.__dict__[pn[0]])
for pn in self.params]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
params = []
for pn, pt in self.params:
pv = getattr(self, pn)
if callable(pv):
params.append('%s=%s' % (pn, validator_to_str(pv)))
else:
params.append('%s=%r' % (pn, pv))
self.argstr = ', '.join(params)
def __call__(self, value):
return self.check(self.valuetype(value))
def convert(self, value):
# transforms the 'internal' representation into the 'external'
return self.valuetype(value)
def __repr__(self):
return self.to_string()
def to_string(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class floatrange(Validator):
@ -102,22 +106,6 @@ class intrange(Validator):
(value, self.lower, self.upper))
class positive(Validator):
def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be > 0!' % value)
class nonnegative(Validator):
def check(self, value):
if value >= 0:
return value
raise ValueError('Value %r must be >= 0!' % value)
class array(Validator):
"""integral amount of data-elements which are described by the SAME validator
@ -129,12 +117,15 @@ class array(Validator):
def check(self, values):
requested_size = len(values)
try:
allowed_size = self.size(requested_size)
except ValueError as e:
raise ValueError(
'illegal number of elements %d, need %r: (%s)' %
(requested_size, self.size, e))
if callable(self.size):
try:
allowed_size = self.size(requested_size)
except ValueError as e:
raise ValueError(
'illegal number of elements %d, need %r: (%s)' %
(requested_size, self.size, e))
else:
allowed_size = self.size
if requested_size != allowed_size:
raise ValueError(
'need %d elements (got %d)' %
@ -152,8 +143,9 @@ class array(Validator):
# more complicated validator may not be able to use validator base class
class vector(object):
class vector(Validator):
"""fixed length, eache element has its own validator"""
valuetype = tuple
def __init__(self, *args):
self.validators = args
@ -165,33 +157,30 @@ class vector(object):
len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class record(object):
# XXX: fixme!
class record(Validator):
"""fixed length, eache element has its own name and validator"""
def __init__(self, **kwds):
self.validators = args
self.argstr = ', '.join([validator_to_str(e) for e in kwds.items()])
self.validators = kwds
self.argstr = ', '.join(
['%s=%s' % (e[0], validator_to_str(e[1])) for e in kwds.items()])
def __call__(self, arg):
def __call__(self, **args):
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class oneof(object):
class oneof(Validator):
"""needs to comply with one of the given validators/values"""
def __init__(self, *args):
self.oneof = args
self.argstr = ', '.join([validator_to_str(e) for e in args])
self.argstr = ', '.join(
[validator_to_str(e) if callable(e) else repr(e) for e in args])
def __call__(self, arg):
for v in self.oneof:
@ -206,11 +195,8 @@ class oneof(object):
return v
raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class enum(object):
class enum(Validator):
def __init__(self, *args, **kwds):
self.mapping = {}
@ -226,8 +212,11 @@ class enum(object):
self.mapping[args.pop(0)] = i
# generate reverse mapping too for use by protocol
self.revmapping = {}
for k, v in self.mapping.items():
params = []
for k, v in sorted(self.mapping.items(), key=lambda x: x[1]):
self.revmapping[v] = k
params.append('%s=%r' % (k, v))
self.argstr = ', '.join(params)
def __call__(self, obj):
try:
@ -238,22 +227,68 @@ class enum(object):
return obj
if obj in self.revmapping:
return self.revmapping[obj]
raise ValueError("%r should be one of %r" %
(obj, list(self.mapping.keys())))
def __repr__(self):
params = ['%s=%r' % (mname, mval)
for mname, mval in self.mapping.items()]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
raise ValueError("%r should be one of %s" %
(obj, ', '.join(map(repr, self.mapping.keys()))))
def convert(self, arg):
return self.mapping.get(arg, arg)
# Validators without parameters:
def positive(value=Ellipsis):
if value != Ellipsis:
if value > 0:
return value
raise ValueError('Value %r must be > 0!' % value)
return -1e-38 # small number > 0
positive.__repr__ = lambda x: validator_to_str(x)
def nonnegative(value=Ellipsis):
if value != Ellipsis:
if value >= 0:
return value
raise ValueError('Value %r must be >= 0!' % value)
return 0.0
nonnegative.__repr__ = lambda x: validator_to_str(x)
# helpers
def validator_to_str(validator):
return str(validator) if not isinstance(validator, type) \
else validator.__name__
if isinstance(validator, Validator):
return validator.to_string()
if hasattr(validator, 'func_name'):
return getattr(validator, 'func_name')
for s in 'int str float'.split(' '):
t = eval(s)
if validator == t or isinstance(validator, t):
return s
print "##########", type(validator), repr(validator)
# XXX: better use a mapping here!
def validator_from_str(validator_str):
return eval(validator_str)
if __name__ == '__main__':
print "minimal testing: validators"
for val, good, bad in [(floatrange(3.09, 5.47), 4.13, 9.27),
(intrange(3, 5), 4, 8),
(array(size=3, datatype=int), (1, 2, 3), (1, 2, 3, 4)),
(vector(int, int), (12, 6), (1.23, 'X')),
(oneof('a', 'b', 'c', 1), 'b', 'x'),
#(record(a=int, b=float), dict(a=2,b=3.97), dict(c=9,d='X')),
(positive, 2, 0),
(nonnegative, 0, -1),
(enum(a=1, b=20), 1, 12),
]:
print validator_to_str(val), repr(validator_from_str(validator_to_str(val)))
print val(good), 'OK'
try:
val(bad)
print "FAIL"
raise ProgrammingError
except Exception as e:
print bad, e, 'OK'
print