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 <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-03-14 13:05:37 +01:00
parent 31b1a916f5
commit 349c510555
6 changed files with 89 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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