diff --git a/bin/secop-gui b/bin/secop-gui index 206a97c..9215c72 100755 --- a/bin/secop-gui +++ b/bin/secop-gui @@ -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) diff --git a/etc/demo.cfg b/etc/demo.cfg index 83f95a4..ecab565 100644 --- a/etc/demo.cfg +++ b/etc/demo.cfg @@ -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] diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index 3180be6..e23c433 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -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]) diff --git a/secop/devices/core.py b/secop/devices/core.py index f475761..2af8a63 100644 --- a/secop/devices/core.py +++ b/secop/devices/core.py @@ -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): diff --git a/secop/devices/demo.py b/secop/devices/demo.py index efa891b..ade76b0 100644 --- a/secop/devices/demo.py +++ b/secop/devices/demo.py @@ -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,), } diff --git a/secop/devices/test.py b/secop/devices/test.py index d5a4916..4e5179c 100644 --- a/secop/devices/test.py +++ b/secop/devices/test.py @@ -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): diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py index 67727a0..7e4a338 100644 --- a/secop/gui/mainwindow.py +++ b/secop/gui/mainwindow.py @@ -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: diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index dc52c39..3cc9c28 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -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) diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index ab5f788..529ef58 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -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('Request: ' '%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 = '
%s
' % Qt.escape(str(msg)).replace('\n', '
') + if error: + msg = '
%s
' % Qt.escape( + str(msg)).replace('\n', '
') + else: + msg = '
%s
' % Qt.escape(str(msg) + ).replace('\n', '
') content = '' if self.logTextBrowser.toPlainText(): @@ -89,4 +101,3 @@ class NodeCtrl(QWidget): # due to monospace) result = self.logTextBrowser.width() / fontMetrics.width('a') return result - diff --git a/secop/gui/paramview.py b/secop/gui/paramview.py new file mode 100644 index 0000000..5a8b306 --- /dev/null +++ b/secop/gui/paramview.py @@ -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 +# +# ***************************************************************************** + +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])) diff --git a/secop/gui/ui/parambuttons.ui b/secop/gui/ui/parambuttons.ui index 2ec97d4..19dec40 100644 --- a/secop/gui/ui/parambuttons.ui +++ b/secop/gui/ui/parambuttons.ui @@ -7,7 +7,7 @@ 0 0 730 - 31 + 33 @@ -33,7 +33,7 @@ - false + true @@ -41,6 +41,9 @@ 0 + + true + diff --git a/secop/gui/ui/paramview.ui b/secop/gui/ui/paramview.ui new file mode 100644 index 0000000..bbe9976 --- /dev/null +++ b/secop/gui/ui/paramview.ui @@ -0,0 +1,99 @@ + + + Form + + + + 0 + 0 + 230 + 121 + + + + Form + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 75 + false + true + + + + Parameter name: + + + + + + + TextLabel + + + + + + + + + Parameters: + + + + 0 + + + 6 + + + 0 + + + 6 + + + 6 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index c10595a..f12cfa9 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -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 diff --git a/secop/protocol/encoding/demo_v3.py b/secop/protocol/encoding/demo_v3.py index ab0b682..b8e8daf 100644 --- a/secop/protocol/encoding/demo_v3.py +++ b/secop/protocol/encoding/demo_v3.py @@ -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, diff --git a/secop/protocol/encoding/demo_v4.py b/secop/protocol/encoding/demo_v4.py index 3177350..4034f74 100644 --- a/secop/protocol/encoding/demo_v4.py +++ b/secop/protocol/encoding/demo_v4.py @@ -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, diff --git a/secop/protocol/errors.py b/secop/protocol/errors.py index b2e9da5..d0df948 100644 --- a/secop/protocol/errors.py +++ b/secop/protocol/errors.py @@ -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....") diff --git a/secop/protocol/messages.py b/secop/protocol/messages.py index a89dbcc..4e09f8d 100644 --- a/secop/protocol/messages.py +++ b/secop/protocol/messages.py @@ -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! diff --git a/secop/validators.py b/secop/validators.py index dfa6377..fa1b1dc 100644 --- a/secop/validators.py +++ b/secop/validators.py @@ -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