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', '
%s
%s' % Qt.escape(str(msg) + ).replace('\n', '