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
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)

View File

@ -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)

View File

@ -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())
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, '<not supported>')), {})
self.update_status(self._get('status', (400, '<not supported>')))
# 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:
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))

View File

@ -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):

View File

@ -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)