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:
parent
31b1a916f5
commit
349c510555
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user