From 349c510555b7088ea229312c41431b92a1c0ba52 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 14 Mar 2023 13:05:37 +0100 Subject: [PATCH] gui: support proper formatting of values - use Datatype.format_value to convert all values - frappy.client.ProxyClient: use CacheItem instead of 3-tuple - CacheItem has built in formatting - adapt gui to use it instead of stopgap As it is now easy to convert to string including values, it may be better to move the unit in the modulewidget into the value field. This would simplyfy the code. Change-Id: I5c06da4a24706fcbc83ebcbf8c0ea6a8eb6d7890 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30680 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- frappy/client/__init__.py | 90 +++++++++++++++++++++++++++++------ frappy/gui/connection.py | 9 ++-- frappy/gui/moduleoverview.py | 20 +++----- frappy/gui/modulewidget.py | 8 +--- frappy/gui/params/__init__.py | 7 +-- frappy/gui/util.py | 20 -------- 6 files changed, 89 insertions(+), 65 deletions(-) diff --git a/frappy/client/__init__.py b/frappy/client/__init__.py index 642ba8c..6b6fc15 100644 --- a/frappy/client/__init__.py +++ b/frappy/client/__init__.py @@ -33,7 +33,7 @@ from threading import Event, RLock, current_thread import frappy.errors import frappy.params from frappy.datatypes import get_datatype -from frappy.lib import mkthread +from frappy.lib import mkthread, formatExtendedStack from frappy.lib.asynconn import AsynConn, ConnectionClosed from frappy.protocol.interface import decode_msg, encode_msg_frame from frappy.protocol.messages import COMMANDREQUEST, \ @@ -99,10 +99,64 @@ class CallbackObject: """ +class CacheItem(tuple): + """cache entry + + includes formatting information + inheriting from tuple: compatible with old previous version of cache + """ + def __new__(cls, value, timestamp=None, readerror=None, datatype=None): + if readerror: + assert isinstance(readerror, Exception) + else: + try: + value = datatype.import_value(value) + except (KeyError, ValueError, AttributeError): + readerror = ValueError('can not import %r as %r' % (value, datatype)) + value = None + obj = tuple.__new__(cls, (value, timestamp, readerror)) + try: + obj.format_value = datatype.format_value + except AttributeError: + obj.format_value = lambda value, unit=None: str(value) + return obj + + @property + def value(self): + return self[0] + + @property + def timestamp(self): + return self[1] + + @property + def readerror(self): + return self[2] + + def __str__(self): + """format value without unit""" + if self[2]: # readerror + return repr(self[2]) + return self.format_value(self[0], unit='') # skip unit + + def formatted(self): + """format value with using unit""" + return self.format_value(self[0]) + + def __repr__(self): + args = (self.value,) + if self.timestamp: + args += (self.timestamp,) + if self.readerror: + args += (self.readerror,) + return 'CacheItem%s' % repr(args) + + class ProxyClient: """common functionality for proxy clients""" - CALLBACK_NAMES = ('updateEvent', 'descriptiveDataChange', 'nodeStateChange', 'unhandledMessage') + CALLBACK_NAMES = ('updateEvent', 'updateItem', 'descriptiveDataChange', + 'nodeStateChange', 'unhandledMessage') online = False # connected or reconnecting since a short time state = 'disconnected' # further possible values: 'connecting', 'reconnecting', 'connected' log = None @@ -133,7 +187,19 @@ class ProxyClient: cbdict[key].append(cbfunc) # immediately call for some callback types - if cbname == 'updateEvent': + if cbname == 'updateItem': + if key is None: + for (mname, pname), data in self.cache.items(): + cbfunc(mname, pname, data) + else: + data = self.cache.get(key, None) + if data: + cbfunc(*key, data) # case single parameter + else: # case key = module + for (mname, pname), data in self.cache.items(): + if mname == key: + cbfunc(mname, pname, data) + elif cbname == 'updateEvent': if key is None: for (mname, pname), data in self.cache.items(): cbfunc(mname, pname, *data) @@ -176,17 +242,13 @@ class ProxyClient: return bool(cblist) def updateValue(self, module, param, value, timestamp, readerror): - if readerror: - assert isinstance(readerror, Exception) - else: - try: - # try to import (needed for enum, scaled, blob) - datatype = self.modules[module]['parameters'][param]['datatype'] - value = datatype.import_value(value) - except (KeyError, ValueError): - if self.log: - self.log.warning('cannot assign %r to %s:%s', value, module, param) - self.cache[(module, param)] = (value, timestamp, readerror) + entry = CacheItem(value, timestamp, readerror, + self.modules[module]['parameters'][param]['datatype']) + self.cache[(module, param)] = entry + self.callback(None, 'updateItem', module, param, entry) + self.callback(module, 'updateItem', module, param, entry) + self.callback((module, param), 'updateItem', module, param, entry) + # TODO: change clients to use updateItem instead of updateEvent self.callback(None, 'updateEvent', module, param, value, timestamp, readerror) self.callback(module, 'updateEvent', module, param, value, timestamp, readerror) self.callback((module, param), 'updateEvent', module, param, value, timestamp, readerror) diff --git a/frappy/gui/connection.py b/frappy/gui/connection.py index 21ecb9d..d6c95ff 100644 --- a/frappy/gui/connection.py +++ b/frappy/gui/connection.py @@ -24,7 +24,6 @@ from frappy.gui.qt import QObject, pyqtSignal import frappy.client -from frappy.gui.util import Value class QSECNode(QObject): @@ -48,7 +47,7 @@ class QSECNode(QObject): self.properties = self.conn.properties self.protocolVersion = conn.secop_version self.log.debug('SECoP Version: %s', conn.secop_version) - conn.register_callback(None, self.updateEvent, self.nodeStateChange, + conn.register_callback(None, self.updateItem, self.nodeStateChange, self.unhandledMessage) # provide methods from old baseclient for making other gui code work @@ -84,7 +83,7 @@ class QSECNode(QObject): return self.conn.execCommand(module, command, argument) def queryCache(self, module): - return {k: Value(*self.conn.cache[(module, k)]) + return {k: self.conn.cache[(module, k)] for k in self.modules[module]['parameters']} def syncCommunicate(self, action, ident='', data=None): @@ -100,8 +99,8 @@ class QSECNode(QObject): # print(module, parameter, self.modules[module]['parameters']) return self.modules[module]['parameters'][parameter] - def updateEvent(self, module, parameter, value, timestamp, readerror): - self.newData.emit(module, parameter, Value(value, timestamp, readerror)) + def updateItem(self, module, parameter, item): + self.newData.emit(module, parameter, item) def nodeStateChange(self, online, state): self.stateChange.emit(self.nodename, online, state) diff --git a/frappy/gui/moduleoverview.py b/frappy/gui/moduleoverview.py index 4fa3e76..2649bde 100644 --- a/frappy/gui/moduleoverview.py +++ b/frappy/gui/moduleoverview.py @@ -24,10 +24,7 @@ class ModuleItem(QTreeWidgetItem): self._hasTarget = 'target' in parameters #if self._hasTarget: # self.setFlags(self.flags() | Qt.ItemIsEditable) - if 'value' in parameters: - props = node.getProperties(self.module, 'value') - self._unit = props.get('unit', '') - else: + if 'status' not in parameters: self.setIcon(self.display['status'], ModuleItem.icons['clear']) self.setText(0, self.module) @@ -72,16 +69,14 @@ class ModuleItem(QTreeWidgetItem): if parameter not in self.display: return if parameter == 'status': - self.setIcon(self.display[parameter], ModuleItem.statusIcon(value.value[0].value)) - self.setText(self.display['status/text'], value.value[1]) - else: - # TODO: stopgap if value.readerror: - strvalue = str(value) + self.setIcon(self.display[parameter], ModuleItem.statusIcon(400)) # 400=ERROR + self.setText(self.display['status/text'], str(value.readerror)) else: - strvalue = ('%g' if isinstance(value.value, float) - else '%s') % (value.value,) - self.setText(self.display[parameter], '%s %s' % (strvalue, self._unit)) + self.setIcon(self.display[parameter], ModuleItem.statusIcon(value.value[0].value)) + self.setText(self.display['status/text'], value.value[1]) + else: + self.setText(self.display[parameter], value.formatted()) def disconnected(self): self.setIcon(self.display['status'], ModuleItem.icons['unknown']) @@ -92,7 +87,6 @@ class ModuleItem(QTreeWidgetItem): def hasTarget(self): return self._hasTarget - def _rebuildAdvanced(self, advanced): if advanced: self.addChildren(self.params) diff --git a/frappy/gui/modulewidget.py b/frappy/gui/modulewidget.py index 6f4a728..b8c3197 100644 --- a/frappy/gui/modulewidget.py +++ b/frappy/gui/modulewidget.py @@ -259,13 +259,7 @@ class ModuleWidget(QWidget): if mod != self._name: return if param in self._paramDisplays: - # TODO: stopgap - if val.readerror: - strvalue = str(val) - else: - strvalue = ('%g' if isinstance(val.value, float) - else '%s') % (val.value,) - self._paramDisplays[param].setText(strvalue) + self._paramDisplays[param].setText(str(val)) def _addParam(self, param, row): paramProps = self._node.getProperties(self._name, param) diff --git a/frappy/gui/params/__init__.py b/frappy/gui/params/__init__.py index e1d7821..2ecdabc 100644 --- a/frappy/gui/params/__init__.py +++ b/frappy/gui/params/__init__.py @@ -77,12 +77,7 @@ class GenericParameterWidget(ParameterWidget): self.setLineEdit.text()) def updateValue(self, value): - fmtstr = getattr(self._datatype, 'fmtstr', '%s') - if value.readerror: - value = str(value) - else: - value = fmtstr % (value.value,) - self.currentLineEdit.setText(value) + self.currentLineEdit.setText(str(value)) class EnumParameterWidget(GenericParameterWidget): diff --git a/frappy/gui/util.py b/frappy/gui/util.py index 8e7a2e7..154f352 100644 --- a/frappy/gui/util.py +++ b/frappy/gui/util.py @@ -32,26 +32,6 @@ 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) - def is_light_theme(palette): background = palette.window().color().lightness()