Skip to content

Commit 193fafa

Browse files
committed
bug fixes and cosmetic tweaks
- Fixed a crash in `gui/base_instrument/InstrumentSortFilterProxyModel` that occurred when "trashed" parameters are "hidden" and the "refreshAll" button was clicked. - Removed duplicate parameter update calls in `gui/instruments`. (can be tested with `dummy_instruments/generic.DummyInstrumentTimeout`) - Ensured the server address is passed correctly to the `SubClient` in `gui/instruments/ModelParameters`, so that parameters in the GUIs can still get the update signal properly when server address is not the default 'localhost:5555'. Useful when the subscriber is not on the server PC. - Made the "instrument type" (in `gui/instruments.GenericInstrument`) still display useful class name when the instrument is a proxy. - Made the gui display floating-point numbers in scientific notation in `gui/parameters` - Made the get-only paramters also update properly when `ParameterWidget` is initialized - updated `log.QLogHandler` to highlight parameter changes, also made the it thread-safe with a html signal-slot - Added some application icons
1 parent ce4190d commit 193fafa

File tree

8 files changed

+487
-23
lines changed

8 files changed

+487
-23
lines changed

instrumentserver/gui/base_instrument.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -
426426
# Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion
427427
if item is not None:
428428
assert isinstance(item, ItemBase)
429-
if self._isParentTrash(parent) or item.trash:
429+
if self._isParentTrash(parent) or getattr(item, "trash", False): # item could be None when it's trashed and hidden
430430
return False
431431

432432
return super().filterAcceptsRow(source_row, source_parent)

instrumentserver/gui/instruments.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from . import parameters, keepSmallHorizontally
1111
from .base_instrument import InstrumentDisplayBase, ItemBase, InstrumentModelBase, InstrumentTreeViewBase, DelegateBase
1212
from .parameters import ParameterWidget, AnyInput, AnyInputForMethod
13-
from .. import QtWidgets, QtCore, QtGui
13+
from .. import QtWidgets, QtCore, QtGui, DEFAULT_PORT
1414
from ..blueprints import ParameterBroadcastBluePrint
1515
from ..client import ProxyInstrument, SubClient
1616
from ..helpers import stringToArgsAndKwargs, nestedAttributeFromString
@@ -252,6 +252,14 @@ def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOption
252252

253253
ret = ParameterWidget(element, widget)
254254
self.parameters[item.name] = ret
255+
# Try to fetch and display current value immediately
256+
# ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ----
257+
# if element.gettable:
258+
# try:
259+
# val = element.get()
260+
# ret._setMethod(val)
261+
# except Exception as e:
262+
# logger.warning(f"Failed to get value for parameter {element.name}: {e}")
255263
return ret
256264

257265

@@ -261,14 +269,17 @@ class ModelParameters(InstrumentModelBase):
261269
itemNewValue = QtCore.Signal(object, object)
262270

263271
def __init__(self, *args, **kwargs):
272+
# make sure we pass the server ip and port properly to the subscriber when the values are not defaults.
273+
subClientArgs = {"sub_host": kwargs.pop("sub_host", 'localhost'),
274+
"sub_port": kwargs.pop("sub_port", DEFAULT_PORT + 1)}
264275
super().__init__(*args, **kwargs)
265276

266277
self.setColumnCount(3)
267278
self.setHorizontalHeaderLabels([self.attr, 'unit', ''])
268279

269280
# Live updates items
270281
self.cliThread = QtCore.QThread()
271-
self.subClient = SubClient([self.instrument.name])
282+
self.subClient = SubClient([self.instrument.name], **subClientArgs)
272283
self.subClient.moveToThread(self.cliThread)
273284

274285
self.cliThread.started.connect(self.subClient.connect)
@@ -292,7 +303,8 @@ def updateParameter(self, bp: ParameterBroadcastBluePrint):
292303
elif bp.action == 'parameter-update' or bp.action == 'parameter-call':
293304
item = self.findItems(fullName, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0)
294305
if len(item) == 0:
295-
self.addItem(fullName, element=nestedAttributeFromString(self.instrument, fullName))
306+
if fullName not in self.itemsHide:
307+
self.addItem(fullName, element=nestedAttributeFromString(self.instrument, fullName))
296308
else:
297309
assert isinstance(item[0], ItemBase)
298310
# The model can't actually modify the widget since it knows nothing about the view itself.
@@ -331,7 +343,8 @@ def __init__(self, model, *args, **kwargs):
331343
def onItemNewValue(self, itemName, value):
332344
widget = self.delegate.parameters[itemName]
333345
try:
334-
widget.paramWidget.setValue(value)
346+
# use the abstract set method defined in parameter widget so it works for different types of widgets
347+
widget._setMethod(value)
335348
except RuntimeError as e:
336349
logger.debug(f"Could not set value for {itemName} to {value}. Object is not being shown right now.")
337350

@@ -348,6 +361,12 @@ def __init__(self, instrument, viewType=ParametersTreeView, callSignals: bool =
348361
if 'parameters-hide' in kwargs:
349362
modelKwargs['itemsHide'] = kwargs.pop('parameters-hide')
350363

364+
# parameters for realtime update subscriber
365+
if 'sub_host' in kwargs:
366+
modelKwargs['sub_host'] = kwargs.pop('sub_host')
367+
if 'sub_port' in kwargs:
368+
modelKwargs['sub_port'] = kwargs.pop('sub_port')
369+
351370
super().__init__(instrument=instrument,
352371
attr='parameters',
353372
itemType=ItemParameters,
@@ -636,6 +655,20 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model
636655

637656
self.ins = ins
638657

658+
if type(ins) is ProxyInstrument:
659+
inst_type = "Proxy-" + ins.bp.instrument_module_class.split('.')[-1]
660+
else:
661+
inst_type = ins.__class__.__name__
662+
663+
ins_label = f'{ins.name} | type: {inst_type}'
664+
665+
try:
666+
# added a unique device_id if the instrument has that method
667+
device_id = ins.device_id()
668+
ins_label += f' | id: {device_id}'
669+
except AttributeError:
670+
pass
671+
639672
self._layout = QtWidgets.QVBoxLayout(self)
640673
self.setLayout(self._layout)
641674

@@ -644,9 +677,15 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model
644677

645678
self.parametersList = InstrumentParameters(instrument=ins, **modelKwargs)
646679
self.methodsList = InstrumentMethods(instrument=ins, **modelKwargs)
647-
self.instrumentNameLabel = QtWidgets.QLabel(f'{self.ins.name} | type: {type(self.ins)}')
680+
self.instrumentNameLabel = QtWidgets.QLabel(ins_label)
648681

649682
self._layout.addWidget(self.instrumentNameLabel)
650683
self._layout.addWidget(self.splitter)
651684
self.splitter.addWidget(self.parametersList)
652685
self.splitter.addWidget(self.methodsList)
686+
# self.parametersList.model.refreshAll() # Chao: removed as we will call that later in the constructor of the param widget
687+
688+
# Resize param name, unit, and function name columns after entries loaded
689+
self.parametersList.view.resizeColumnToContents(0)
690+
self.parametersList.view.resizeColumnToContents(1)
691+
self.methodsList.view.resizeColumnToContents(0)

instrumentserver/gui/parameters.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
22
import math
33
import numbers
4-
from typing import Any, Optional, List, Callable
4+
from typing import Any, Optional, List
5+
import re
56

67
from qcodes import Parameter
78

@@ -15,6 +16,20 @@
1516

1617
# TODO: do all styling with a global style sheet
1718

19+
FLOAT_PRECISION = 10 # The maximum number of significant digits for float numbers
20+
21+
def float_formater(val):
22+
"""
23+
For displaying float numbers with scientific notation.
24+
"""
25+
if isinstance(val, float):
26+
if abs(val) > 1e5 or (0 < abs(val) < 1e-4):
27+
formatted = f"{val:.{FLOAT_PRECISION - 1}g}"
28+
# remove leading 0 in exponent
29+
formatted = re.sub(r"e([+-])0(\d+)", r"e\1\2", formatted)
30+
return formatted
31+
return str(val)
32+
1833

1934
class ParameterWidget(QtWidgets.QWidget):
2035
"""A widget that allows editing and/or displaying a parameter value."""
@@ -35,7 +50,7 @@ class ParameterWidget(QtWidgets.QWidget):
3550
_valueFromWidget = QtCore.Signal(object)
3651

3752
def __init__(self, parameter: Parameter, parent=None,
38-
additionalWidgets: Optional[List[QtWidgets.QWidget]] = []):
53+
additionalWidgets: Optional[List[QtWidgets.QWidget]] = None):
3954

4055
super().__init__(parent)
4156

@@ -128,6 +143,10 @@ def __init__(self, parameter: Parameter, parent=None,
128143
self.paramWidget = QtWidgets.QLabel(self)
129144
self._setMethod = lambda x: self.paramWidget.setText(str(x)) \
130145
if isinstance(self.paramWidget, QtWidgets.QLabel) else None
146+
try: # also do immediate update for read-only params, as what we do for the editable parameters above.
147+
self._setMethod(parameter())
148+
except Exception as e:
149+
logger.warning(f"Error when setting parameter {parameter}: {e}", exc_info=True)
131150

132151
layout.addWidget(self.paramWidget, 0, 0)
133152
additionalWidgets = additionalWidgets or []
@@ -217,7 +236,7 @@ def value(self):
217236

218237
def setValue(self, val: Any):
219238
try:
220-
self.input.setText(str(val))
239+
self.input.setText(float_formater(val))
221240
except RuntimeError as e:
222241
logger.debug(f"Could not set value {val} in AnyInput element does not exists, raised {type(e)}: {e.args}")
223242

@@ -260,7 +279,7 @@ def value(self):
260279

261280
def setValue(self, value: numbers.Number):
262281
try:
263-
self.setText(str(value))
282+
self.setText(float_formater(value))
264283
except RuntimeError as e:
265284
logger.debug(f"Could not set value {value} in NumberInput, raised {type(e)}: {e.args}")
266285

instrumentserver/log.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import sys
66
import logging
77
from enum import Enum, auto, unique
8+
from html import escape
9+
import re
810

9-
from . import QtGui, QtWidgets
11+
from . import QtGui, QtWidgets, QtCore
1012

1113

1214
@unique
@@ -17,7 +19,7 @@ class LogLevels(Enum):
1719
debug = auto()
1820

1921

20-
class QLogHandler(logging.Handler):
22+
class QLogHandler(QtCore.QObject,logging.Handler):
2123
"""A simple log handler that supports logging in TextEdit"""
2224

2325
COLORS = {
@@ -26,21 +28,71 @@ class QLogHandler(logging.Handler):
2628
logging.INFO: QtGui.QColor('green'),
2729
logging.DEBUG: QtGui.QColor('gray'),
2830
}
31+
32+
new_html = QtCore.Signal(str)
2933

3034
def __init__(self, parent):
31-
super().__init__()
35+
QtCore.QObject.__init__(self, parent)
36+
logging.Handler.__init__(self)
37+
3238
self.widget = QtWidgets.QTextEdit(parent)
3339
self.widget.setReadOnly(True)
34-
35-
def emit(self, record):
36-
msg = self.format(record)
37-
clr = self.COLORS.get(record.levelno, QtGui.QColor('black'))
38-
self.widget.setTextColor(clr)
39-
self.widget.append(msg)
40+
self._transform = None
41+
42+
# connect signal to slot that actually touches the widget (GUI thread)
43+
self.new_html.connect(self._append_html)
44+
45+
46+
@QtCore.Slot(str)
47+
def _append_html(self, html: str):
48+
"""Append HTML to the text widget in the GUI thread."""
49+
self.widget.append(html)
50+
# reset char format so bold/italics don’t bleed into the next line
51+
self.widget.setCurrentCharFormat(QtGui.QTextCharFormat())
52+
# keep view scrolled to bottom
4053
self.widget.verticalScrollBar().setValue(
4154
self.widget.verticalScrollBar().maximum()
4255
)
4356

57+
58+
def set_transform(self, fn):
59+
"""fn(record, msg) -> str | {'html': str} | None"""
60+
self._transform = fn
61+
62+
def emit(self, record):
63+
formatted = self.format(record) # prefix + message
64+
raw_msg = record.getMessage() # message only
65+
66+
# Color for prefix (log level)
67+
clr = self.COLORS.get(record.levelno, QtGui.QColor('black')).name()
68+
69+
if self._transform is not None:
70+
html_fragment = self._transform(record, raw_msg)
71+
if html_fragment:
72+
i = formatted.rfind(raw_msg)
73+
if i >= 0:
74+
prefix = formatted[:i]
75+
suffix = formatted[i + len(raw_msg):]
76+
else:
77+
prefix, suffix = "", ""
78+
79+
# Build HTML line
80+
html = (
81+
f"<span style='color:{clr}'>{escape(prefix)}</span>"
82+
f"{html_fragment}"
83+
f"{escape(suffix)}"
84+
)
85+
86+
# send to GUI thread
87+
self.new_html.emit(html)
88+
return
89+
90+
# fallback: original plain text path
91+
msg = formatted
92+
clr_q = self.COLORS.get(record.levelno, QtGui.QColor('black')).name()
93+
html = f"<span style='color:{clr_q}'>{escape(msg)}</span>"
94+
95+
self.new_html.emit(html)
4496

4597
class LogWidget(QtWidgets.QWidget):
4698
"""
@@ -58,6 +110,7 @@ def __init__(self, parent=None, level=logging.INFO):
58110
logTextBox = QLogHandler(self)
59111
logTextBox.setFormatter(fmt)
60112
logTextBox.setLevel(level)
113+
self.handler = logTextBox
61114

62115
# make the widget
63116
layout = QtWidgets.QVBoxLayout()
@@ -77,6 +130,27 @@ def __init__(self, parent=None, level=logging.INFO):
77130
# del h
78131

79132
self.logger.addHandler(logTextBox)
133+
self.handler.set_transform(_param_update_formatter)
134+
135+
136+
def _param_update_formatter(record, raw_msg):
137+
"""
138+
A formater that makes parameter updates more prominent in the gui log window.
139+
"""
140+
# Pattern 1: "parameter-update" from the broadcaster, for client station
141+
pattern_update = re.compile(r'parameter-update:\s*([A-Za-z0-9_.]+):\s*(.+)', re.S)
142+
143+
# Pattern 2: normal log message from the server. i.e. `Parameter {name} set to: {value}`
144+
pattern_info = re.compile(r"Parameter\s+'([A-Za-z0-9_.]+)'\s+set\s+to:\s*(.+)", re.S)
145+
146+
match = pattern_update.search(raw_msg) or pattern_info.search(raw_msg)
147+
if not match:
148+
return None
149+
150+
name, value = match.groups()
151+
152+
# Escape HTML but keep \n literal (QTextEdit.append will render them)
153+
return ( f"<b>{escape(name)}</b> set to: " f"<span style='color:#7e5bef; font-weight:bold'>{escape(value)}</span>" )
80154

81155

82156
def setupLogging(addStreamHandler=True, logFile=None,

0 commit comments

Comments
 (0)