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.errors
import frappy.params import frappy.params
from frappy.datatypes import get_datatype 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.lib.asynconn import AsynConn, ConnectionClosed
from frappy.protocol.interface import decode_msg, encode_msg_frame from frappy.protocol.interface import decode_msg, encode_msg_frame
from frappy.protocol.messages import COMMANDREQUEST, \ 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: class ProxyClient:
"""common functionality for proxy clients""" """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 online = False # connected or reconnecting since a short time
state = 'disconnected' # further possible values: 'connecting', 'reconnecting', 'connected' state = 'disconnected' # further possible values: 'connecting', 'reconnecting', 'connected'
log = None log = None
@ -133,7 +187,19 @@ class ProxyClient:
cbdict[key].append(cbfunc) cbdict[key].append(cbfunc)
# immediately call for some callback types # 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: if key is None:
for (mname, pname), data in self.cache.items(): for (mname, pname), data in self.cache.items():
cbfunc(mname, pname, *data) cbfunc(mname, pname, *data)
@ -176,17 +242,13 @@ class ProxyClient:
return bool(cblist) return bool(cblist)
def updateValue(self, module, param, value, timestamp, readerror): def updateValue(self, module, param, value, timestamp, readerror):
if readerror: entry = CacheItem(value, timestamp, readerror,
assert isinstance(readerror, Exception) self.modules[module]['parameters'][param]['datatype'])
else: self.cache[(module, param)] = entry
try: self.callback(None, 'updateItem', module, param, entry)
# try to import (needed for enum, scaled, blob) self.callback(module, 'updateItem', module, param, entry)
datatype = self.modules[module]['parameters'][param]['datatype'] self.callback((module, param), 'updateItem', module, param, entry)
value = datatype.import_value(value) # TODO: change clients to use updateItem instead of updateEvent
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)
self.callback(None, 'updateEvent', module, param, value, timestamp, readerror) self.callback(None, 'updateEvent', module, param, value, timestamp, readerror)
self.callback(module, '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) self.callback((module, param), 'updateEvent', module, param, value, timestamp, readerror)

View File

@ -24,7 +24,6 @@
from frappy.gui.qt import QObject, pyqtSignal from frappy.gui.qt import QObject, pyqtSignal
import frappy.client import frappy.client
from frappy.gui.util import Value
class QSECNode(QObject): class QSECNode(QObject):
@ -48,7 +47,7 @@ class QSECNode(QObject):
self.properties = self.conn.properties self.properties = self.conn.properties
self.protocolVersion = conn.secop_version self.protocolVersion = conn.secop_version
self.log.debug('SECoP Version: %s', 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) self.unhandledMessage)
# provide methods from old baseclient for making other gui code work # 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) return self.conn.execCommand(module, command, argument)
def queryCache(self, module): 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']} for k in self.modules[module]['parameters']}
def syncCommunicate(self, action, ident='', data=None): def syncCommunicate(self, action, ident='', data=None):
@ -100,8 +99,8 @@ class QSECNode(QObject):
# print(module, parameter, self.modules[module]['parameters']) # print(module, parameter, self.modules[module]['parameters'])
return self.modules[module]['parameters'][parameter] return self.modules[module]['parameters'][parameter]
def updateEvent(self, module, parameter, value, timestamp, readerror): def updateItem(self, module, parameter, item):
self.newData.emit(module, parameter, Value(value, timestamp, readerror)) self.newData.emit(module, parameter, item)
def nodeStateChange(self, online, state): def nodeStateChange(self, online, state):
self.stateChange.emit(self.nodename, online, state) self.stateChange.emit(self.nodename, online, state)

View File

@ -24,10 +24,7 @@ class ModuleItem(QTreeWidgetItem):
self._hasTarget = 'target' in parameters self._hasTarget = 'target' in parameters
#if self._hasTarget: #if self._hasTarget:
# self.setFlags(self.flags() | Qt.ItemIsEditable) # self.setFlags(self.flags() | Qt.ItemIsEditable)
if 'value' in parameters: if 'status' not in parameters:
props = node.getProperties(self.module, 'value')
self._unit = props.get('unit', '')
else:
self.setIcon(self.display['status'], ModuleItem.icons['clear']) self.setIcon(self.display['status'], ModuleItem.icons['clear'])
self.setText(0, self.module) self.setText(0, self.module)
@ -72,16 +69,14 @@ class ModuleItem(QTreeWidgetItem):
if parameter not in self.display: if parameter not in self.display:
return return
if parameter == 'status': if parameter == 'status':
if value.readerror:
self.setIcon(self.display[parameter], ModuleItem.statusIcon(400)) # 400=ERROR
self.setText(self.display['status/text'], str(value.readerror))
else:
self.setIcon(self.display[parameter], ModuleItem.statusIcon(value.value[0].value)) self.setIcon(self.display[parameter], ModuleItem.statusIcon(value.value[0].value))
self.setText(self.display['status/text'], value.value[1]) self.setText(self.display['status/text'], value.value[1])
else: else:
# TODO: stopgap self.setText(self.display[parameter], value.formatted())
if value.readerror:
strvalue = str(value)
else:
strvalue = ('%g' if isinstance(value.value, float)
else '%s') % (value.value,)
self.setText(self.display[parameter], '%s %s' % (strvalue, self._unit))
def disconnected(self): def disconnected(self):
self.setIcon(self.display['status'], ModuleItem.icons['unknown']) self.setIcon(self.display['status'], ModuleItem.icons['unknown'])
@ -92,7 +87,6 @@ class ModuleItem(QTreeWidgetItem):
def hasTarget(self): def hasTarget(self):
return self._hasTarget return self._hasTarget
def _rebuildAdvanced(self, advanced): def _rebuildAdvanced(self, advanced):
if advanced: if advanced:
self.addChildren(self.params) self.addChildren(self.params)

View File

@ -259,13 +259,7 @@ class ModuleWidget(QWidget):
if mod != self._name: if mod != self._name:
return return
if param in self._paramDisplays: if param in self._paramDisplays:
# TODO: stopgap self._paramDisplays[param].setText(str(val))
if val.readerror:
strvalue = str(val)
else:
strvalue = ('%g' if isinstance(val.value, float)
else '%s') % (val.value,)
self._paramDisplays[param].setText(strvalue)
def _addParam(self, param, row): def _addParam(self, param, row):
paramProps = self._node.getProperties(self._name, param) paramProps = self._node.getProperties(self._name, param)

View File

@ -77,12 +77,7 @@ class GenericParameterWidget(ParameterWidget):
self.setLineEdit.text()) self.setLineEdit.text())
def updateValue(self, value): def updateValue(self, value):
fmtstr = getattr(self._datatype, 'fmtstr', '%s') self.currentLineEdit.setText(str(value))
if value.readerror:
value = str(value)
else:
value = fmtstr % (value.value,)
self.currentLineEdit.setText(value)
class EnumParameterWidget(GenericParameterWidget): class EnumParameterWidget(GenericParameterWidget):

View File

@ -32,26 +32,6 @@ 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)
def is_light_theme(palette): def is_light_theme(palette):
background = palette.window().color().lightness() background = palette.window().color().lightness()