diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py index 875537e..d7a6e22 100644 --- a/secop/gui/mainwindow.py +++ b/secop/gui/mainwindow.py @@ -24,13 +24,14 @@ import sys -from secop.client.baseclient import Client as SECNode +import secop.client from secop.gui.modulectrl import ModuleCtrl from secop.gui.nodectrl import NodeCtrl from secop.gui.paramview import ParameterView from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \ - QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot -from secop.gui.util import loadUi + QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor +from secop.gui.util import loadUi, Value +from secop.lib import formatExtendedTraceback ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1 ITEM_TYPE_GROUP = QTreeWidgetItem.UserType + 2 @@ -38,34 +39,78 @@ ITEM_TYPE_MODULE = QTreeWidgetItem.UserType + 3 ITEM_TYPE_PARAMETER = QTreeWidgetItem.UserType + 4 -class QSECNode(SECNode, QObject): +class QSECNode(QObject): newData = pyqtSignal(str, str, object) # module, parameter, data + stateChange = pyqtSignal(str, bool, str) # node name, online, connection state + unhandledMsg = pyqtSignal(str) # message + logEntry = pyqtSignal(str) - def __init__(self, opts, autoconnect=False, parent=None): - SECNode.__init__(self, opts, autoconnect) + def __init__(self, uri, parent=None): QObject.__init__(self, parent) + self.conn = conn = secop.client.SecopClient(uri) + conn.validate_data = True + self.log = conn.log + self.contactPoint = conn.uri + conn.connect() + self.equipmentId = conn.properties['equipment_id'] + self.nodename = '%s (%s)' % (self.equipmentId, conn.uri) + self.modules = conn.modules + self.properties = self.conn.properties + self.protocolVersion = conn.secop_version + conn.register(None, self) # generic callback - self.startup(True) - self._subscribeCallbacks() + # provide methods from old baseclient for making other gui code work - def _subscribeCallbacks(self): - for module in self.modules: - self._subscribeModuleCallback(module) + def getParameters(self, module): + return self.modules[module]['parameters'] - def _subscribeModuleCallback(self, module): - for parameter in self.getParameters(module): - self._subscribeParameterCallback(module, parameter) + def getCommands(self, module): + return self.modules[module]['commands'] - def _subscribeParameterCallback(self, module, parameter): - self.register_callback(module, parameter, self._newDataReceived) + def getModuleProperties(self, module): + return self.modules[module]['properties'] - def _newDataReceived(self, module, parameter, data): - self.newData.emit(module, parameter, data) + def getProperties(self, module, parameter): + props = self.modules[module]['parameters'][parameter] + if 'unit' in props['datainfo']: + props['unit'] = props['datainfo']['unit'] + return self.modules[module]['parameters'][parameter] + + def setParameter(self, module, parameter, value): + self.conn.setParameter(module, parameter, value) + + def getParameter(self, module, parameter): + return self.conn.getParameter(module, parameter, True) + + def execCommand(self, module, command, arg): + return self.conn.execCommand(module, command, arg) + + def queryCache(self, module): + return {k: Value(*self.conn.cache[(module, k)]) + for k in self.modules[module]['parameters']} + + def syncCommunicate(self, action, ident='', data=None): + reply = self.conn.request(action, ident, data) + return secop.client.encode_msg_frame(*reply).decode('utf-8') + + def decode_message(self, msg): + # decode_msg needs bytes as input + return secop.client.decode_msg(msg.encode('utf-8')) + + def _getDescribingParameterData(self, module, parameter): + return self.modules[module]['parameters'][parameter] + + def updateEvent(self, module, parameter, value, timestamp, readerror): + self.newData.emit(module, parameter, Value(value, timestamp, readerror)) + + def nodeStateChange(self, online, state): + self.stateChange.emit(self.nodename, online, state) + + def unhandledMessage(self, *msg): + self.unhandledMsg.emit('%s %s %r' % msg) class MainWindow(QMainWindow): - askReopenSignal = pyqtSignal(str, str) - def __init__(self, parent=None): super(MainWindow, self).__init__(parent) @@ -83,7 +128,6 @@ class MainWindow(QMainWindow): self._paramCtrls = {} self._topItems = {} self._currentWidget = self.splitter.widget(1).layout().takeAt(0) - self.askReopenSignal.connect(self.askReopen) # add localhost (if available) and SEC nodes given as arguments args = sys.argv[1:] @@ -95,7 +139,8 @@ class MainWindow(QMainWindow): try: self._addNode(host) except Exception as e: - print(e) + print(formatExtendedTraceback()) + print('error in addNode: %r' % e) @pyqtSlot() def on_actionAdd_SEC_node_triggered(self): @@ -132,32 +177,24 @@ class MainWindow(QMainWindow): def _removeSubTree(self, toplevel_item): self.treeWidget.invisibleRootItem().removeChild(toplevel_item) - def _nodeDisconnected_callback(self, nodename, host): + def _set_node_state(self, nodename, online, state): node = self._nodes[nodename] - self._removeSubTree(self._topItems[node]) - del self._topItems[node] - node.quit() - self.askReopenSignal.emit(nodename, host) - - def askReopen(self, nodename, host): - result = QMessageBox.question(self.parent(), 'connection closed', - 'connection to %s closed, reopen?' % nodename) - if result == QMessageBox.Yes: - self._addNode(host) + if online: + self._topItems[node].setBackground(0, QBrush(QColor('white'))) + else: + self._topItems[node].setBackground(0, QBrush(QColor('orange'))) + # TODO: make connection state be a separate row + node.contactPoint = '%s (%s)' % (node.conn.uri, state) + if nodename in self._nodeCtrls: + self._nodeCtrls[nodename].contactPointLabel.setText(node.contactPoint) def _addNode(self, host): # create client - port = 10767 - if ':' in host: - host, port = host.split(':', 1) - port = int(port) - node = QSECNode({'host': host, 'port': port}, parent=self) - host = '%s:%d' % (host, port) + node = QSECNode(host, parent=self) + nodename = node.nodename - nodename = '%s (%s)' % (node.equipmentId, host) self._nodes[nodename] = node - node.register_shutdown_callback(self._nodeDisconnected_callback, nodename, host) # fill tree nodeItem = QTreeWidgetItem(None, [nodename], ITEM_TYPE_NODE) @@ -171,11 +208,13 @@ class MainWindow(QMainWindow): self.treeWidget.addTopLevelItem(nodeItem) self._topItems[node] = nodeItem + node.stateChange.connect(self._set_node_state) def _displayNode(self, node): ctrl = self._nodeCtrls.get(node, None) if ctrl is None: ctrl = self._nodeCtrls[node] = NodeCtrl(self._nodes[node]) + self._nodes[node].unhandledMsg.connect(ctrl._addLogEntry) self._replaceCtrlWidget(ctrl) diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index 3e2d9f0..700e25b 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -65,8 +65,7 @@ class CommandDialog(QDialog): def showCommandResultDialog(command, args, result, extras=''): m = QMessageBox() - if not args: - args = '' + args = '' if args is None else repr(args) m.setText('calling: %s(%s)\nyielded: %r\nqualifiers: %s' % (command, args, result, extras)) m.exec_() @@ -159,8 +158,6 @@ class ModuleCtrl(QWidget): self._node.newData.connect(self._updateValue) def _execCommand(self, command, args=None): - if not args: - args = tuple() try: result, qualifiers = self._node.execCommand( self._module, command, args) @@ -222,7 +219,7 @@ class ModuleCtrl(QWidget): 'datatype', None) # yes: create a widget for this as well labelstr, buttons = self._makeEntry( - group, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True) + group, initValues[param], datatype=datatype, nolabel=True, checkbox=checkbox, invert=True) checkbox.setText(labelstr) # add to Layout (yes: ignore the label!) @@ -240,7 +237,7 @@ class ModuleCtrl(QWidget): initval = None print("Warning: %r not in initValues!" % param_) else: - initval = initValues[param_].value + initval = initValues[param_] datatype = self._node.getProperties( self._module, param_).get( 'datatype', None) @@ -261,7 +258,7 @@ class ModuleCtrl(QWidget): self._module, param).get( 'datatype', None) label, buttons = self._makeEntry( - param, initValues[param].value, datatype=datatype) + param, initValues[param], datatype=datatype) # add to Layout self.paramGroupBox.layout().addWidget(label, row, 0) @@ -359,7 +356,7 @@ class ModuleCtrl(QWidget): def _updateValue(self, module, parameter, value): if module != self._module: return - # value is [data, qualifiers] + # value is is type secop.gui.mainwindow.Value # note: update subwidgets with the data portion only # note: paramwidgets[..][1] is a ParameterView from secop.gui.params - self._paramWidgets[parameter][1].updateValue(value[0]) + self._paramWidgets[parameter][1].updateValue(value) diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index b60fdbd..e6d0278 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -31,7 +31,8 @@ from secop.datatypes import EnumType, StringType from secop.errors import SECoPError from secop.gui.qt import QFont, QFontMetrics, QLabel, \ QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped -from secop.gui.util import loadUi +from secop.gui.util import loadUi, Value +import secop.lib class NodeCtrl(QWidget): @@ -45,13 +46,15 @@ class NodeCtrl(QWidget): self.contactPointLabel.setText(self._node.contactPoint) self.equipmentIdLabel.setText(self._node.equipmentId) self.protocolVersionLabel.setText(self._node.protocolVersion) - self.nodeDescriptionLabel.setText(self._node.describingData['properties'].get( - 'description', 'no description available')) + self.nodeDescriptionLabel.setText(self._node.properties.get('description', + 'no description available')) self._clearLog() # now populate modules tab self._init_modules_tab() + node.logEntry.connect(self._addLogEntry) + @pyqtSlot() def on_sendPushButton_clicked(self): msg = self.msgLineEdit.text().strip() @@ -72,12 +75,19 @@ class NodeCtrl(QWidget): stuff, indent=2, separators=(',', ':'), sort_keys=True)) self._addLogEntry(reply, newline=True, pretty=False) else: - self._addLogEntry(reply, newline=True, pretty=True) + self._addLogEntry(reply, newline=True, pretty=False) except SECoPError as e: + einfo = e.args[0] if len(e.args) == 1 else json.dumps(e.args) self._addLogEntry( - 'error %s %s' % (e.name, json.dumps(e.args)), + '%s: %s' % (e.name, einfo), newline=True, - pretty=True, + pretty=False, + error=True) + except Exception as e: + self._addLogEntry( + 'error when sending %r: %r' % (msg, e), + newline=True, + pretty=False, error=True) @pyqtSlot() @@ -154,6 +164,7 @@ class NodeCtrl(QWidget): else: widget = QLabel('Unsupported Interfaceclass %r' % interfaces) except Exception as e: + print(secop.lib.formatExtendedTraceback()) widget = QLabel('Bad configured Module %s! (%s)' % (modname, e)) @@ -184,16 +195,19 @@ class ReadableWidget(QWidget): # XXX: avoid a nasty race condition, mainly biting on M$ for i in range(15): - if 'status' in self._node.describing_data['modules'][module]['accessibles']: + if 'status' in self._node.modules[module]['parameters']: break sleep(0.01*i) self._status_type = self._node.getProperties( self._module, 'status').get('datatype') - params = self._node.getProperties(self._module, 'value') - datatype = params.get('datatype', StringType()) - self._is_enum = isinstance(datatype, EnumType) + try: + props = self._node.getProperties(self._module, 'target') + datatype = props.get('datatype', StringType()) + self._is_enum = isinstance(datatype, EnumType) + except KeyError: + self._is_enum = False loadUi(self, 'modulebuttons.ui') @@ -214,37 +228,26 @@ class ReadableWidget(QWidget): self._node.newData.connect(self._updateValue) def _get(self, pname, fallback=Ellipsis): - params = self._node.queryCache(self._module) - if pname in params: - return params[pname].value try: - # if queried, we get the qualifiers as well, but don't want them - # here + return Value(*self._node.getParameter(self._module, pname)) + except Exception as e: + # happens only, if there is no response form read request mlzlog.getLogger('cached values').warn( - 'no cached value for %s:%s' % (self._module, pname)) - val = self._node.getParameter(self._module, pname)[0] - return val - except Exception: - self._node.log.exception() - if fallback is not Ellipsis: - return fallback - raise + 'no cached value for %s:%s %r' % (self._module, pname, e)) + return Value(fallback) def _init_status_widgets(self): - self.update_status(self._get('status', (999, '')), {}) + self.update_status(self._get('status', (400, ''))) # XXX: also connect update_status signal to LineEdit ?? - def update_status(self, status, qualifiers=None): - display_string = self._status_type.members[0]._enum[status[0]].name - if status[1]: - display_string += ':' + status[1] - self.statusLineEdit.setText(display_string) + def update_status(self, status): + self.statusLineEdit.setText(str(status)) # may change meaning of cmdPushButton def _init_current_widgets(self): - self.update_current(self._get('value', ''), {}) + self.update_current(self._get('value', '')) - def update_current(self, value, qualifiers=None): + def update_current(self, value): self.currentLineEdit.setText(str(value)) def _init_target_widgets(self): @@ -253,18 +256,18 @@ class ReadableWidget(QWidget): self.targetComboBox.setHidden(True) self.cmdPushButton.setHidden(True) - def update_target(self, target, qualifiers=None): + def update_target(self, target): pass def _updateValue(self, module, parameter, value): if module != self._module: return if parameter == 'status': - self.update_status(*value) + self.update_status(value) elif parameter == 'value': - self.update_current(*value) + self.update_current(value) elif parameter == 'target': - self.update_target(*value) + self.update_target(value) class DrivableWidget(ReadableWidget): @@ -278,28 +281,30 @@ class DrivableWidget(ReadableWidget): # normal types: disable Combobox self.targetComboBox.setHidden(True) target = self._get('target', None) - if target: - if isinstance(target, list) and isinstance(target[1], dict): - self.update_target(target[0]) + if target.value is not None: + if isinstance(target.value, list) and isinstance(target.value[1], dict): + self.update_target(Value(target.value[0])) else: self.update_target(target) - def update_current(self, value, qualifiers=None): - if self._is_enum: - member = self._map[self._revmap[value]] - self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value)) - else: - self.currentLineEdit.setText(str(value)) + def update_current(self, value): + self.currentLineEdit.setText(str(value)) + #elif self._is_enum: + # member = self._map[self._revmap[value.value]] + # self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value)) - def update_target(self, target, qualifiers=None): + def update_target(self, target): if self._is_enum: + if target.readerror: + return # update selected item - if target in self._revmap: - self.targetComboBox.setCurrentIndex(self._revmap[target]) + value = target.value + if value in self._revmap: + self.targetComboBox.setCurrentIndex(self._revmap[value]) else: print( "%s: Got invalid target value %r!" % - (self._module, target)) + (self._module, value)) else: self.targetLineEdit.setText(str(target)) diff --git a/secop/gui/params/__init__.py b/secop/gui/params/__init__.py index a461dff..18fc959 100644 --- a/secop/gui/params/__init__.py +++ b/secop/gui/params/__init__.py @@ -79,8 +79,6 @@ class GenericParameterWidget(ParameterWidget): self.setLineEdit.text()) def updateValue(self, value): - if self._datatype: - value = self._datatype.import_value(value) self.currentLineEdit.setText(str(value)) @@ -113,12 +111,7 @@ class EnumParameterWidget(GenericParameterWidget): self.setRequested.emit(self._module, self._paramcmd, member) def updateValue(self, value): - try: - member = self._map[self._revmap[int(value)]] - self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value)) - except Exception: - self.currentLineEdit.setText('undefined Value: %r' % value) - print(formatExtendedStack()) + self.currentLineEdit.setText(str(value)) class GenericCmdWidget(ParameterWidget): diff --git a/secop/gui/util.py b/secop/gui/util.py index 377bf41..4e24b62 100644 --- a/secop/gui/util.py +++ b/secop/gui/util.py @@ -31,3 +31,23 @@ uipath = path.dirname(__file__) def loadUi(widget, uiname, subdir='ui'): uic.loadUi(path.join(uipath, subdir, uiname), widget) + +class Value: + def __init__(self, value, timestamp=None, readerror=None): + self.value = value + self.timestamp = timestamp + self.readerror = readerror + + def __str__(self): + """for display""" + if self.readerror: + return str('!!' + str(self.readerror) + '!!') + return str(self.value) + + def __repr__(self): + args = (self.value,) + if self.timestamp: + args += (self.timestamp,) + if self.readerror: + args += (self.readerror,) + return 'Value%s' % repr(args)