Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion instrumentserver/gui/base_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -
# Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion
if item is not None:
assert isinstance(item, ItemBase)
if self._isParentTrash(parent) or item.trash:
if self._isParentTrash(parent) or getattr(item, "trash", False): # item could be None when it's trashed and hidden
return False

return super().filterAcceptsRow(source_row, source_parent)
Expand Down
49 changes: 44 additions & 5 deletions instrumentserver/gui/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from . import parameters, keepSmallHorizontally
from .base_instrument import InstrumentDisplayBase, ItemBase, InstrumentModelBase, InstrumentTreeViewBase, DelegateBase
from .parameters import ParameterWidget, AnyInput, AnyInputForMethod
from .. import QtWidgets, QtCore, QtGui
from .. import QtWidgets, QtCore, QtGui, DEFAULT_PORT
from ..blueprints import ParameterBroadcastBluePrint
from ..client import ProxyInstrument, SubClient
from ..helpers import stringToArgsAndKwargs, nestedAttributeFromString
Expand Down Expand Up @@ -252,6 +252,14 @@ def createEditor(self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOption

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


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

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

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

# Live updates items
self.cliThread = QtCore.QThread()
self.subClient = SubClient([self.instrument.name])
self.subClient = SubClient([self.instrument.name], **subClientArgs)
self.subClient.moveToThread(self.cliThread)

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

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

# parameters for realtime update subscriber
if 'sub_host' in kwargs:
modelKwargs['sub_host'] = kwargs.pop('sub_host')
if 'sub_port' in kwargs:
modelKwargs['sub_port'] = kwargs.pop('sub_port')

super().__init__(instrument=instrument,
attr='parameters',
itemType=ItemParameters,
Expand Down Expand Up @@ -636,6 +655,20 @@ def __init__(self, ins: Union[ProxyInstrument, Instrument], parent=None, **model

self.ins = ins

if type(ins) is ProxyInstrument:
inst_type = "Proxy-" + ins.bp.instrument_module_class.split('.')[-1]
else:
inst_type = ins.__class__.__name__

ins_label = f'{ins.name} | type: {inst_type}'

try:
# added a unique device_id if the instrument has that method
device_id = ins.device_id()
ins_label += f' | id: {device_id}'
except AttributeError:
pass

self._layout = QtWidgets.QVBoxLayout(self)
self.setLayout(self._layout)

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

self.parametersList = InstrumentParameters(instrument=ins, **modelKwargs)
self.methodsList = InstrumentMethods(instrument=ins, **modelKwargs)
self.instrumentNameLabel = QtWidgets.QLabel(f'{self.ins.name} | type: {type(self.ins)}')
self.instrumentNameLabel = QtWidgets.QLabel(ins_label)

self._layout.addWidget(self.instrumentNameLabel)
self._layout.addWidget(self.splitter)
self.splitter.addWidget(self.parametersList)
self.splitter.addWidget(self.methodsList)


# Resize param name, unit, and function name columns after entries loaded
self.parametersList.view.resizeColumnToContents(0)
self.parametersList.view.resizeColumnToContents(1)
self.methodsList.view.resizeColumnToContents(0)
27 changes: 23 additions & 4 deletions instrumentserver/gui/parameters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
import math
import numbers
from typing import Any, Optional, List, Callable
from typing import Any, Optional, List
import re

from qcodes import Parameter

Expand All @@ -15,6 +16,20 @@

# TODO: do all styling with a global style sheet

FLOAT_PRECISION = 10 # The maximum number of significant digits for float numbers

def float_formater(val):
"""
For displaying float numbers with scientific notation.
"""
if isinstance(val, float):
if abs(val) > 1e5 or (0 < abs(val) < 1e-4):
formatted = f"{val:.{FLOAT_PRECISION - 1}g}"
# remove leading 0 in exponent
formatted = re.sub(r"e([+-])0(\d+)", r"e\1\2", formatted)
return formatted
return str(val)


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

def __init__(self, parameter: Parameter, parent=None,
additionalWidgets: Optional[List[QtWidgets.QWidget]] = []):
additionalWidgets: Optional[List[QtWidgets.QWidget]] = None):

super().__init__(parent)

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

layout.addWidget(self.paramWidget, 0, 0)
additionalWidgets = additionalWidgets or []
Expand Down Expand Up @@ -217,7 +236,7 @@ def value(self):

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

Expand Down Expand Up @@ -260,7 +279,7 @@ def value(self):

def setValue(self, value: numbers.Number):
try:
self.setText(str(value))
self.setText(float_formater(value))
except RuntimeError as e:
logger.debug(f"Could not set value {value} in NumberInput, raised {type(e)}: {e.args}")

Expand Down
92 changes: 83 additions & 9 deletions instrumentserver/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import sys
import logging
from enum import Enum, auto, unique
from html import escape
import re

from . import QtGui, QtWidgets
from . import QtGui, QtWidgets, QtCore


@unique
Expand All @@ -17,7 +19,7 @@ class LogLevels(Enum):
debug = auto()


class QLogHandler(logging.Handler):
class QLogHandler(QtCore.QObject,logging.Handler):
"""A simple log handler that supports logging in TextEdit"""

COLORS = {
Expand All @@ -26,21 +28,71 @@ class QLogHandler(logging.Handler):
logging.INFO: QtGui.QColor('green'),
logging.DEBUG: QtGui.QColor('gray'),
}

new_html = QtCore.Signal(str)

def __init__(self, parent):
super().__init__()
QtCore.QObject.__init__(self, parent)
logging.Handler.__init__(self)

self.widget = QtWidgets.QTextEdit(parent)
self.widget.setReadOnly(True)

def emit(self, record):
msg = self.format(record)
clr = self.COLORS.get(record.levelno, QtGui.QColor('black'))
self.widget.setTextColor(clr)
self.widget.append(msg)
self._transform = None

# connect signal to slot that actually touches the widget (GUI thread)
self.new_html.connect(self._append_html)


@QtCore.Slot(str)
def _append_html(self, html: str):
"""Append HTML to the text widget in the GUI thread."""
self.widget.append(html)
# reset char format so bold/italics don’t bleed into the next line
self.widget.setCurrentCharFormat(QtGui.QTextCharFormat())
# keep view scrolled to bottom
self.widget.verticalScrollBar().setValue(
self.widget.verticalScrollBar().maximum()
)


def set_transform(self, fn):
"""fn(record, msg) -> str | {'html': str} | None"""
self._transform = fn

def emit(self, record):
formatted = self.format(record) # prefix + message
raw_msg = record.getMessage() # message only

# Color for prefix (log level)
clr = self.COLORS.get(record.levelno, QtGui.QColor('black')).name()

if self._transform is not None:
html_fragment = self._transform(record, raw_msg)
if html_fragment:
i = formatted.rfind(raw_msg)
if i >= 0:
prefix = formatted[:i]
suffix = formatted[i + len(raw_msg):]
else:
prefix, suffix = "", ""

# Build HTML line
html = (
f"<span style='color:{clr}'>{escape(prefix)}</span>"
f"{html_fragment}"
f"{escape(suffix)}"
)

# send to GUI thread
self.new_html.emit(html)
return

# fallback: original plain text path
msg = formatted
clr_q = self.COLORS.get(record.levelno, QtGui.QColor('black')).name()
html = f"<span style='color:{clr_q}'>{escape(msg)}</span>"

self.new_html.emit(html)

class LogWidget(QtWidgets.QWidget):
"""
Expand All @@ -58,6 +110,7 @@ def __init__(self, parent=None, level=logging.INFO):
logTextBox = QLogHandler(self)
logTextBox.setFormatter(fmt)
logTextBox.setLevel(level)
self.handler = logTextBox

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

self.logger.addHandler(logTextBox)
self.handler.set_transform(_param_update_formatter)


def _param_update_formatter(record, raw_msg):
"""
A formater that makes parameter updates more prominent in the gui log window.
"""
# Pattern 1: "parameter-update" from the broadcaster, for client station
pattern_update = re.compile(r'parameter-update:\s*([A-Za-z0-9_.]+):\s*(.+)', re.S)

# Pattern 2: normal log message from the server. i.e. `Parameter {name} set to: {value}`
pattern_info = re.compile(r"Parameter\s+'([A-Za-z0-9_.]+)'\s+set\s+to:\s*(.+)", re.S)

match = pattern_update.search(raw_msg) or pattern_info.search(raw_msg)
if not match:
return None

name, value = match.groups()

# Escape HTML but keep \n literal (QTextEdit.append will render them)
return ( f"<b>{escape(name)}</b> set to: " f"<span style='color:#7e5bef; font-weight:bold'>{escape(value)}</span>" )


def setupLogging(addStreamHandler=True, logFile=None,
Expand Down
Loading