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:
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user