secop-gui based on secop.client.Client

instead of secop.client.baseclient.Client

Change-Id: I869a3a9ecba40382908b4741ef055a0c5afe018f
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22471
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2020-02-17 12:34:13 +01:00
parent 685f22330a
commit 8cf4c2d8eb
5 changed files with 160 additions and 106 deletions

View File

@ -24,13 +24,14 @@
import sys import sys
from secop.client.baseclient import Client as SECNode import secop.client
from secop.gui.modulectrl import ModuleCtrl from secop.gui.modulectrl import ModuleCtrl
from secop.gui.nodectrl import NodeCtrl from secop.gui.nodectrl import NodeCtrl
from secop.gui.paramview import ParameterView from secop.gui.paramview import ParameterView
from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \ from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \
QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor
from secop.gui.util import loadUi from secop.gui.util import loadUi, Value
from secop.lib import formatExtendedTraceback
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1 ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
ITEM_TYPE_GROUP = QTreeWidgetItem.UserType + 2 ITEM_TYPE_GROUP = QTreeWidgetItem.UserType + 2
@ -38,34 +39,78 @@ ITEM_TYPE_MODULE = QTreeWidgetItem.UserType + 3
ITEM_TYPE_PARAMETER = QTreeWidgetItem.UserType + 4 ITEM_TYPE_PARAMETER = QTreeWidgetItem.UserType + 4
class QSECNode(SECNode, QObject): class QSECNode(QObject):
newData = pyqtSignal(str, str, object) # module, parameter, data 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): def __init__(self, uri, parent=None):
SECNode.__init__(self, opts, autoconnect)
QObject.__init__(self, parent) 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) # provide methods from old baseclient for making other gui code work
self._subscribeCallbacks()
def _subscribeCallbacks(self): def getParameters(self, module):
for module in self.modules: return self.modules[module]['parameters']
self._subscribeModuleCallback(module)
def _subscribeModuleCallback(self, module): def getCommands(self, module):
for parameter in self.getParameters(module): return self.modules[module]['commands']
self._subscribeParameterCallback(module, parameter)
def _subscribeParameterCallback(self, module, parameter): def getModuleProperties(self, module):
self.register_callback(module, parameter, self._newDataReceived) return self.modules[module]['properties']
def _newDataReceived(self, module, parameter, data): def getProperties(self, module, parameter):
self.newData.emit(module, parameter, data) 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): class MainWindow(QMainWindow):
askReopenSignal = pyqtSignal(str, str)
def __init__(self, parent=None): def __init__(self, parent=None):
super(MainWindow, self).__init__(parent) super(MainWindow, self).__init__(parent)
@ -83,7 +128,6 @@ class MainWindow(QMainWindow):
self._paramCtrls = {} self._paramCtrls = {}
self._topItems = {} self._topItems = {}
self._currentWidget = self.splitter.widget(1).layout().takeAt(0) self._currentWidget = self.splitter.widget(1).layout().takeAt(0)
self.askReopenSignal.connect(self.askReopen)
# add localhost (if available) and SEC nodes given as arguments # add localhost (if available) and SEC nodes given as arguments
args = sys.argv[1:] args = sys.argv[1:]
@ -95,7 +139,8 @@ class MainWindow(QMainWindow):
try: try:
self._addNode(host) self._addNode(host)
except Exception as e: except Exception as e:
print(e) print(formatExtendedTraceback())
print('error in addNode: %r' % e)
@pyqtSlot() @pyqtSlot()
def on_actionAdd_SEC_node_triggered(self): def on_actionAdd_SEC_node_triggered(self):
@ -132,32 +177,24 @@ class MainWindow(QMainWindow):
def _removeSubTree(self, toplevel_item): def _removeSubTree(self, toplevel_item):
self.treeWidget.invisibleRootItem().removeChild(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] node = self._nodes[nodename]
self._removeSubTree(self._topItems[node]) if online:
del self._topItems[node] self._topItems[node].setBackground(0, QBrush(QColor('white')))
node.quit() else:
self.askReopenSignal.emit(nodename, host) self._topItems[node].setBackground(0, QBrush(QColor('orange')))
# TODO: make connection state be a separate row
def askReopen(self, nodename, host): node.contactPoint = '%s (%s)' % (node.conn.uri, state)
result = QMessageBox.question(self.parent(), 'connection closed', if nodename in self._nodeCtrls:
'connection to %s closed, reopen?' % nodename) self._nodeCtrls[nodename].contactPointLabel.setText(node.contactPoint)
if result == QMessageBox.Yes:
self._addNode(host)
def _addNode(self, host): def _addNode(self, host):
# create client # create client
port = 10767 node = QSECNode(host, parent=self)
if ':' in host: nodename = node.nodename
host, port = host.split(':', 1)
port = int(port)
node = QSECNode({'host': host, 'port': port}, parent=self)
host = '%s:%d' % (host, port)
nodename = '%s (%s)' % (node.equipmentId, host)
self._nodes[nodename] = node self._nodes[nodename] = node
node.register_shutdown_callback(self._nodeDisconnected_callback, nodename, host)
# fill tree # fill tree
nodeItem = QTreeWidgetItem(None, [nodename], ITEM_TYPE_NODE) nodeItem = QTreeWidgetItem(None, [nodename], ITEM_TYPE_NODE)
@ -171,11 +208,13 @@ class MainWindow(QMainWindow):
self.treeWidget.addTopLevelItem(nodeItem) self.treeWidget.addTopLevelItem(nodeItem)
self._topItems[node] = nodeItem self._topItems[node] = nodeItem
node.stateChange.connect(self._set_node_state)
def _displayNode(self, node): def _displayNode(self, node):
ctrl = self._nodeCtrls.get(node, None) ctrl = self._nodeCtrls.get(node, None)
if ctrl is None: if ctrl is None:
ctrl = self._nodeCtrls[node] = NodeCtrl(self._nodes[node]) ctrl = self._nodeCtrls[node] = NodeCtrl(self._nodes[node])
self._nodes[node].unhandledMsg.connect(ctrl._addLogEntry)
self._replaceCtrlWidget(ctrl) self._replaceCtrlWidget(ctrl)

View File

@ -65,8 +65,7 @@ class CommandDialog(QDialog):
def showCommandResultDialog(command, args, result, extras=''): def showCommandResultDialog(command, args, result, extras=''):
m = QMessageBox() m = QMessageBox()
if not args: args = '' if args is None else repr(args)
args = ''
m.setText('calling: %s(%s)\nyielded: %r\nqualifiers: %s' % m.setText('calling: %s(%s)\nyielded: %r\nqualifiers: %s' %
(command, args, result, extras)) (command, args, result, extras))
m.exec_() m.exec_()
@ -159,8 +158,6 @@ class ModuleCtrl(QWidget):
self._node.newData.connect(self._updateValue) self._node.newData.connect(self._updateValue)
def _execCommand(self, command, args=None): def _execCommand(self, command, args=None):
if not args:
args = tuple()
try: try:
result, qualifiers = self._node.execCommand( result, qualifiers = self._node.execCommand(
self._module, command, args) self._module, command, args)
@ -222,7 +219,7 @@ class ModuleCtrl(QWidget):
'datatype', None) 'datatype', None)
# yes: create a widget for this as well # yes: create a widget for this as well
labelstr, buttons = self._makeEntry( 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) checkbox.setText(labelstr)
# add to Layout (yes: ignore the label!) # add to Layout (yes: ignore the label!)
@ -240,7 +237,7 @@ class ModuleCtrl(QWidget):
initval = None initval = None
print("Warning: %r not in initValues!" % param_) print("Warning: %r not in initValues!" % param_)
else: else:
initval = initValues[param_].value initval = initValues[param_]
datatype = self._node.getProperties( datatype = self._node.getProperties(
self._module, param_).get( self._module, param_).get(
'datatype', None) 'datatype', None)
@ -261,7 +258,7 @@ class ModuleCtrl(QWidget):
self._module, param).get( self._module, param).get(
'datatype', None) 'datatype', None)
label, buttons = self._makeEntry( label, buttons = self._makeEntry(
param, initValues[param].value, datatype=datatype) param, initValues[param], datatype=datatype)
# add to Layout # add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0) self.paramGroupBox.layout().addWidget(label, row, 0)
@ -359,7 +356,7 @@ class ModuleCtrl(QWidget):
def _updateValue(self, module, parameter, value): def _updateValue(self, module, parameter, value):
if module != self._module: if module != self._module:
return return
# value is [data, qualifiers] # value is is type secop.gui.mainwindow.Value
# note: update subwidgets with the data portion only # note: update subwidgets with the data portion only
# note: paramwidgets[..][1] is a ParameterView from secop.gui.params # note: paramwidgets[..][1] is a ParameterView from secop.gui.params
self._paramWidgets[parameter][1].updateValue(value[0]) self._paramWidgets[parameter][1].updateValue(value)

View File

@ -31,7 +31,8 @@ from secop.datatypes import EnumType, StringType
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.gui.qt import QFont, QFontMetrics, QLabel, \ from secop.gui.qt import QFont, QFontMetrics, QLabel, \
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
from secop.gui.util import loadUi from secop.gui.util import loadUi, Value
import secop.lib
class NodeCtrl(QWidget): class NodeCtrl(QWidget):
@ -45,13 +46,15 @@ class NodeCtrl(QWidget):
self.contactPointLabel.setText(self._node.contactPoint) self.contactPointLabel.setText(self._node.contactPoint)
self.equipmentIdLabel.setText(self._node.equipmentId) self.equipmentIdLabel.setText(self._node.equipmentId)
self.protocolVersionLabel.setText(self._node.protocolVersion) self.protocolVersionLabel.setText(self._node.protocolVersion)
self.nodeDescriptionLabel.setText(self._node.describingData['properties'].get( self.nodeDescriptionLabel.setText(self._node.properties.get('description',
'description', 'no description available')) 'no description available'))
self._clearLog() self._clearLog()
# now populate modules tab # now populate modules tab
self._init_modules_tab() self._init_modules_tab()
node.logEntry.connect(self._addLogEntry)
@pyqtSlot() @pyqtSlot()
def on_sendPushButton_clicked(self): def on_sendPushButton_clicked(self):
msg = self.msgLineEdit.text().strip() msg = self.msgLineEdit.text().strip()
@ -72,12 +75,19 @@ class NodeCtrl(QWidget):
stuff, indent=2, separators=(',', ':'), sort_keys=True)) stuff, indent=2, separators=(',', ':'), sort_keys=True))
self._addLogEntry(reply, newline=True, pretty=False) self._addLogEntry(reply, newline=True, pretty=False)
else: else:
self._addLogEntry(reply, newline=True, pretty=True) self._addLogEntry(reply, newline=True, pretty=False)
except SECoPError as e: except SECoPError as e:
einfo = e.args[0] if len(e.args) == 1 else json.dumps(e.args)
self._addLogEntry( self._addLogEntry(
'error %s %s' % (e.name, json.dumps(e.args)), '%s: %s' % (e.name, einfo),
newline=True, 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) error=True)
@pyqtSlot() @pyqtSlot()
@ -154,6 +164,7 @@ class NodeCtrl(QWidget):
else: else:
widget = QLabel('Unsupported Interfaceclass %r' % interfaces) widget = QLabel('Unsupported Interfaceclass %r' % interfaces)
except Exception as e: except Exception as e:
print(secop.lib.formatExtendedTraceback())
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e)) 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$ # XXX: avoid a nasty race condition, mainly biting on M$
for i in range(15): for i in range(15):
if 'status' in self._node.describing_data['modules'][module]['accessibles']: if 'status' in self._node.modules[module]['parameters']:
break break
sleep(0.01*i) sleep(0.01*i)
self._status_type = self._node.getProperties( self._status_type = self._node.getProperties(
self._module, 'status').get('datatype') self._module, 'status').get('datatype')
params = self._node.getProperties(self._module, 'value') try:
datatype = params.get('datatype', StringType()) props = self._node.getProperties(self._module, 'target')
self._is_enum = isinstance(datatype, EnumType) datatype = props.get('datatype', StringType())
self._is_enum = isinstance(datatype, EnumType)
except KeyError:
self._is_enum = False
loadUi(self, 'modulebuttons.ui') loadUi(self, 'modulebuttons.ui')
@ -214,37 +228,26 @@ class ReadableWidget(QWidget):
self._node.newData.connect(self._updateValue) self._node.newData.connect(self._updateValue)
def _get(self, pname, fallback=Ellipsis): def _get(self, pname, fallback=Ellipsis):
params = self._node.queryCache(self._module)
if pname in params:
return params[pname].value
try: try:
# if queried, we get the qualifiers as well, but don't want them return Value(*self._node.getParameter(self._module, pname))
# here except Exception as e:
# happens only, if there is no response form read request
mlzlog.getLogger('cached values').warn( mlzlog.getLogger('cached values').warn(
'no cached value for %s:%s' % (self._module, pname)) 'no cached value for %s:%s %r' % (self._module, pname, e))
val = self._node.getParameter(self._module, pname)[0] return Value(fallback)
return val
except Exception:
self._node.log.exception()
if fallback is not Ellipsis:
return fallback
raise
def _init_status_widgets(self): def _init_status_widgets(self):
self.update_status(self._get('status', (999, '<not supported>')), {}) self.update_status(self._get('status', (400, '<not supported>')))
# XXX: also connect update_status signal to LineEdit ?? # XXX: also connect update_status signal to LineEdit ??
def update_status(self, status, qualifiers=None): def update_status(self, status):
display_string = self._status_type.members[0]._enum[status[0]].name self.statusLineEdit.setText(str(status))
if status[1]:
display_string += ':' + status[1]
self.statusLineEdit.setText(display_string)
# may change meaning of cmdPushButton # may change meaning of cmdPushButton
def _init_current_widgets(self): 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)) self.currentLineEdit.setText(str(value))
def _init_target_widgets(self): def _init_target_widgets(self):
@ -253,18 +256,18 @@ class ReadableWidget(QWidget):
self.targetComboBox.setHidden(True) self.targetComboBox.setHidden(True)
self.cmdPushButton.setHidden(True) self.cmdPushButton.setHidden(True)
def update_target(self, target, qualifiers=None): def update_target(self, target):
pass pass
def _updateValue(self, module, parameter, value): def _updateValue(self, module, parameter, value):
if module != self._module: if module != self._module:
return return
if parameter == 'status': if parameter == 'status':
self.update_status(*value) self.update_status(value)
elif parameter == 'value': elif parameter == 'value':
self.update_current(*value) self.update_current(value)
elif parameter == 'target': elif parameter == 'target':
self.update_target(*value) self.update_target(value)
class DrivableWidget(ReadableWidget): class DrivableWidget(ReadableWidget):
@ -278,28 +281,30 @@ class DrivableWidget(ReadableWidget):
# normal types: disable Combobox # normal types: disable Combobox
self.targetComboBox.setHidden(True) self.targetComboBox.setHidden(True)
target = self._get('target', None) target = self._get('target', None)
if target: if target.value is not None:
if isinstance(target, list) and isinstance(target[1], dict): if isinstance(target.value, list) and isinstance(target.value[1], dict):
self.update_target(target[0]) self.update_target(Value(target.value[0]))
else: else:
self.update_target(target) self.update_target(target)
def update_current(self, value, qualifiers=None): def update_current(self, value):
if self._is_enum: self.currentLineEdit.setText(str(value))
member = self._map[self._revmap[value]] #elif self._is_enum:
self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value)) # member = self._map[self._revmap[value.value]]
else: # self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
self.currentLineEdit.setText(str(value))
def update_target(self, target, qualifiers=None): def update_target(self, target):
if self._is_enum: if self._is_enum:
if target.readerror:
return
# update selected item # update selected item
if target in self._revmap: value = target.value
self.targetComboBox.setCurrentIndex(self._revmap[target]) if value in self._revmap:
self.targetComboBox.setCurrentIndex(self._revmap[value])
else: else:
print( print(
"%s: Got invalid target value %r!" % "%s: Got invalid target value %r!" %
(self._module, target)) (self._module, value))
else: else:
self.targetLineEdit.setText(str(target)) self.targetLineEdit.setText(str(target))

View File

@ -79,8 +79,6 @@ class GenericParameterWidget(ParameterWidget):
self.setLineEdit.text()) self.setLineEdit.text())
def updateValue(self, value): def updateValue(self, value):
if self._datatype:
value = self._datatype.import_value(value)
self.currentLineEdit.setText(str(value)) self.currentLineEdit.setText(str(value))
@ -113,12 +111,7 @@ class EnumParameterWidget(GenericParameterWidget):
self.setRequested.emit(self._module, self._paramcmd, member) self.setRequested.emit(self._module, self._paramcmd, member)
def updateValue(self, value): def updateValue(self, value):
try: self.currentLineEdit.setText(str(value))
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())
class GenericCmdWidget(ParameterWidget): class GenericCmdWidget(ParameterWidget):

View File

@ -31,3 +31,23 @@ uipath = path.dirname(__file__)
def loadUi(widget, uiname, subdir='ui'): def loadUi(widget, uiname, subdir='ui'):
uic.loadUi(path.join(uipath, subdir, uiname), widget) 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)