0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 03:01:50 +02:00

feat: (#494) add signal display to device browser

This commit is contained in:
2025-06-23 14:45:14 +02:00
committed by Jan Wyzula
parent 3a103410e7
commit f3da6e959e
5 changed files with 61 additions and 16 deletions

View File

@ -6,10 +6,9 @@ from bec_lib.atlas_models import Device as DeviceConfigModel
from bec_lib.devicemanager import DeviceContainer from bec_lib.devicemanager import DeviceContainer
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_qthemes import material_icon from bec_qthemes import material_icon
from PySide6.QtWidgets import QTabWidget, QVBoxLayout
from qtpy.QtCore import QMimeData, QSize, Qt, Signal from qtpy.QtCore import QMimeData, QSize, Qt, Signal
from qtpy.QtGui import QDrag from qtpy.QtGui import QDrag
from qtpy.QtWidgets import QApplication, QHBoxLayout, QToolButton, QWidget from qtpy.QtWidgets import QApplication, QHBoxLayout, QTabWidget, QToolButton, QVBoxLayout, QWidget
from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame from bec_widgets.utils.expandable_frame import ExpandableGroupFrame

View File

@ -1,9 +1,10 @@
from bec_qthemes import material_icon
from qtpy.QtCore import Qt from qtpy.QtCore import Qt
from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.widgets.containers.dock.dock import BECDock from bec_widgets.widgets.containers.dock.dock import BECDock
from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
@ -32,14 +33,35 @@ class SignalDisplay(BECWidget, QWidget):
self._device = device self._device = device
self.device = device self.device = device
@SafeSlot()
def _refresh(self):
if self.device in self.dev:
self.dev.get(self.device).read(cached=False)
self.dev.get(self.device).read_configuration(cached=False)
def _add_refresh_button(self):
button_holder = QWidget()
button_holder.setLayout(QHBoxLayout())
button_holder.layout().setAlignment(Qt.AlignmentFlag.AlignRight)
button_holder.layout().setContentsMargins(0, 0, 0, 0)
refresh_button = QToolButton()
refresh_button.setIcon(
material_icon(icon_name="refresh", size=(20, 20), convert_to_pixmap=False)
)
refresh_button.clicked.connect(self._refresh)
button_holder.layout().addWidget(refresh_button)
self._content_layout.addWidget(button_holder)
def _populate(self): def _populate(self):
self._content.deleteLater() self._content.deleteLater()
self._content = QWidget() self._content = QWidget()
self._layout.addWidget(self._content) self._layout.addWidget(self._content)
self._content_layout = QVBoxLayout() self._content_layout = QVBoxLayout()
self._content_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self._content_layout.setContentsMargins(0, 0, 0, 0)
self._content.setLayout(self._content_layout) self._content.setLayout(self._content_layout)
self._add_refresh_button()
if self._device in self.dev: if self._device in self.dev:
for sig in self.dev[self.device]._info.get("signals", {}).keys(): for sig in self.dev[self.device]._info.get("signals", {}).keys():
self._content_layout.addWidget( self._content_layout.addWidget(

View File

@ -179,6 +179,7 @@ class SignalLabel(BECWidget, QWidget):
self._custom_units: str = custom_units self._custom_units: str = custom_units
self._show_default_units: bool = show_default_units self._show_default_units: bool = show_default_units
self._decimal_places = 3 self._decimal_places = 3
self._dtype = None
self._show_hinted_signals: bool = True self._show_hinted_signals: bool = True
self._show_normal_signals: bool = False self._show_normal_signals: bool = False
@ -240,8 +241,10 @@ class SignalLabel(BECWidget, QWidget):
"""Subscribe to the Redis topic for the device to display""" """Subscribe to the Redis topic for the device to display"""
if not self._connected and self._device and self._device in self.dev: if not self._connected and self._device and self._device in self.dev:
self._connected = True self._connected = True
self._readback_endpoint = MessageEndpoints.device_readback(self._device) self._read_endpoint = MessageEndpoints.device_read(self._device)
self.bec_dispatcher.connect_slot(self.on_device_readback, self._readback_endpoint) self._read_config_endpoint = MessageEndpoints.device_read_configuration(self._device)
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_endpoint)
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_config_endpoint)
self._manual_read() self._manual_read()
self.set_display_value(self._value) self.set_display_value(self._value)
@ -249,7 +252,8 @@ class SignalLabel(BECWidget, QWidget):
"""Unsubscribe from the Redis topic for the device to display""" """Unsubscribe from the Redis topic for the device to display"""
if self._connected: if self._connected:
self._connected = False self._connected = False
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._readback_endpoint) self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_endpoint)
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_config_endpoint)
def _manual_read(self): def _manual_read(self):
if self._device is None or not isinstance( if self._device is None or not isinstance(
@ -258,8 +262,13 @@ class SignalLabel(BECWidget, QWidget):
self._units = "" self._units = ""
self._value = "__" self._value = "__"
return return
signal: Signal = ( signal, info = (
getattr(device, self.signal, None) if isinstance(device, Device) else device (
getattr(device, self.signal, None),
device._info.get("signals", {}).get(self._signal, {}).get("describe", {}),
)
if isinstance(device, Device)
else (device, device.describe().get(self._device))
) )
if not isinstance(signal, Signal): # Avoid getting other attributes of device, e.g. methods if not isinstance(signal, Signal): # Avoid getting other attributes of device, e.g. methods
signal = None signal = None
@ -268,7 +277,8 @@ class SignalLabel(BECWidget, QWidget):
self._value = "__" self._value = "__"
return return
self._value = signal.get() self._value = signal.get()
self._units = signal.get_device_config().get("egu", "") self._units = info.get("egu", "")
self._dtype = info.get("dtype", "float")
@SafeSlot(dict, dict) @SafeSlot(dict, dict)
def on_device_readback(self, msg: dict, metadata: dict) -> None: def on_device_readback(self, msg: dict, metadata: dict) -> None:
@ -277,8 +287,10 @@ class SignalLabel(BECWidget, QWidget):
""" """
try: try:
signal_to_read = self._patch_hinted_signal() signal_to_read = self._patch_hinted_signal()
self._value = msg["signals"][signal_to_read]["value"] _value = msg["signals"].get(signal_to_read, {}).get("value")
self.set_display_value(self._value) if _value is not None:
self._value = _value
self.set_display_value(self._value)
except Exception as e: except Exception as e:
self._display.setText("ERROR!") self._display.setText("ERROR!")
self._display.setToolTip( self._display.setToolTip(
@ -400,7 +412,10 @@ class SignalLabel(BECWidget, QWidget):
if self._decimal_places == 0: if self._decimal_places == 0:
return value return value
try: try:
return f"{float(value):0.{self._decimal_places}f}" if self._dtype in ("integer", "float"):
return f"{float(value):0.{self._decimal_places}f}"
else:
return str(value)
except ValueError: except ValueError:
return value return value

View File

@ -3,6 +3,7 @@ from unittest import mock
import pytest import pytest
from qtpy.QtCore import QPoint, Qt from qtpy.QtCore import QPoint, Qt
from qtpy.QtWidgets import QTabWidget
from bec_widgets.widgets.services.device_browser.device_browser import DeviceBrowser from bec_widgets.widgets.services.device_browser.device_browser import DeviceBrowser
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import ( from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
@ -86,8 +87,13 @@ def test_device_item_expansion(device_browser, qtbot):
device_item: QListWidgetItem = device_browser.ui.device_list.itemAt(0, 0) device_item: QListWidgetItem = device_browser.ui.device_list.itemAt(0, 0)
widget: DeviceItem = device_browser.ui.device_list.itemWidget(device_item) widget: DeviceItem = device_browser.ui.device_list.itemWidget(device_item)
qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton) qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton)
form = widget._contents.layout().itemAt(0).widget() tab_widget: QTabWidget = widget._contents.layout().itemAt(0).widget()
qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500) qtbot.waitUntil(lambda: tab_widget.widget(0) is not None, timeout=100)
qtbot.waitUntil(
lambda: isinstance(tab_widget.widget(0).layout().itemAt(0).widget(), DeviceConfigForm),
timeout=100,
)
form = tab_widget.widget(0).layout().itemAt(0).widget()
assert widget.expanded assert widget.expanded
assert (name_field := form.widget_dict.get("name")) is not None assert (name_field := form.widget_dict.get("name")) is not None
qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500) qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500)

View File

@ -121,11 +121,13 @@ def test_custom_label(signal_label: SignalLabel, qtbot):
def test_units_in_display(signal_label: SignalLabel, qtbot): def test_units_in_display(signal_label: SignalLabel, qtbot):
signal_label._value = "1.8" signal_label._value = "1.8"
signal_label._dtype = "float"
signal_label.custom_units = "Mfurlong μfortnight⁻¹" signal_label.custom_units = "Mfurlong μfortnight⁻¹"
assert signal_label._display.text() == "1.800 Mfurlong μfortnight⁻¹" assert signal_label._display.text() == "1.800 Mfurlong μfortnight⁻¹"
def test_decimal_places(signal_label: SignalLabel, qtbot): def test_decimal_places(signal_label: SignalLabel, qtbot):
signal_label._dtype = "float"
signal_label.decimal_places = 2 signal_label.decimal_places = 2
signal_label.set_display_value("123.456") signal_label.set_display_value("123.456")
assert signal_label._display.text() == "123.46 m/s" assert signal_label._display.text() == "123.46 m/s"
@ -226,6 +228,7 @@ def test_handle_readback(signal_label: SignalLabel, qtbot):
signal_label.device = "samx" signal_label.device = "samx"
signal_label.signal = "readback" signal_label.signal = "readback"
signal_label.custom_units = "μm" signal_label.custom_units = "μm"
signal_label._dtype = "float"
signal_label.on_device_readback({"random": {"stuff": "in", "corrupted": "reading"}}, {}) signal_label.on_device_readback({"random": {"stuff": "in", "corrupted": "reading"}}, {})
assert signal_label._display.text() == "ERROR!" assert signal_label._display.text() == "ERROR!"
assert "Error processing incoming reading" in signal_label._display.toolTip() assert "Error processing incoming reading" in signal_label._display.toolTip()