mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 01:37:53 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bcff75107 | ||
| 608590c542 | |||
|
|
012f7cf970 | ||
| cd17a4aad9 | |||
| f0dc992586 | |||
| fd1f9941e0 | |||
| 3384ca02bd | |||
| 959cedbbd5 | |||
| ca4f97503b | |||
| 22beadcad0 | |||
| b9af36a4f1 |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,6 +1,47 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.30.1 (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ignore KeyError in SignalLabel
|
||||
([`608590c`](https://github.com/bec-project/bec_widgets/commit/608590c5421368d5bba0e4b0f5187d90cac323be))
|
||||
|
||||
|
||||
## v2.30.0 (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **device_browser**: Display signal for signals
|
||||
([`3384ca0`](https://github.com/bec-project/bec_widgets/commit/3384ca02bdb5a2798ad3339ecf3e2ba7c121e28f))
|
||||
|
||||
- **device_signal_display**: Don't read omitted
|
||||
([`b9af36a`](https://github.com/bec-project/bec_widgets/commit/b9af36a4f1c91e910d4fc738b17b90e92287a7e3))
|
||||
|
||||
- **signal_label**: Rewrite reading selection logic
|
||||
([`cd17a4a`](https://github.com/bec-project/bec_widgets/commit/cd17a4aad905296eb0460ecc27e5920f5c2e8fe5))
|
||||
|
||||
- **signal_label**: Show all signals by default
|
||||
([`22beadc`](https://github.com/bec-project/bec_widgets/commit/22beadcad061b328c986414f30fef57b64bad693))
|
||||
|
||||
- **signal_label**: Update signal from dialog correctly
|
||||
([`959cedb`](https://github.com/bec-project/bec_widgets/commit/959cedbbd5a123eef5f3370287bf6476c48caab9))
|
||||
|
||||
- **signal_label**: Use read() instead of get() for init
|
||||
([`f0dc992`](https://github.com/bec-project/bec_widgets/commit/f0dc99258607a5cc8af51686d01f7fd54ae2779f))
|
||||
|
||||
### Chores
|
||||
|
||||
- Update client.py
|
||||
([`fd1f994`](https://github.com/bec-project/bec_widgets/commit/fd1f9941e046b7ae1e247dde39c20bcbc37ac189))
|
||||
|
||||
### Features
|
||||
|
||||
- **signal_label**: Property to display array data or not
|
||||
([`ca4f975`](https://github.com/bec-project/bec_widgets/commit/ca4f97503bf06363e8e8a5d494a9857223da4104))
|
||||
|
||||
|
||||
## v2.29.0 (2025-07-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -4378,6 +4378,62 @@ class SignalLabel(RPCBase):
|
||||
Show the button to select the signal to display
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_hinted_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show hinted signals
|
||||
"""
|
||||
|
||||
@show_hinted_signals.setter
|
||||
@rpc_call
|
||||
def show_hinted_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show hinted signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_normal_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show normal signals
|
||||
"""
|
||||
|
||||
@show_normal_signals.setter
|
||||
@rpc_call
|
||||
def show_normal_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show normal signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_config_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show config signals
|
||||
"""
|
||||
|
||||
@show_config_signals.setter
|
||||
@rpc_call
|
||||
def show_config_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show config signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def display_array_data(self) -> "bool":
|
||||
"""
|
||||
Displays the full data from array signals if set to True.
|
||||
"""
|
||||
|
||||
@display_array_data.setter
|
||||
@rpc_call
|
||||
def display_array_data(self) -> "bool":
|
||||
"""
|
||||
Displays the full data from array signals if set to True.
|
||||
"""
|
||||
|
||||
|
||||
class SignalLineEdit(RPCBase):
|
||||
"""Line edit widget for device input with autocomplete for device names."""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from bec_lib.device import Device
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
|
||||
@@ -5,6 +6,7 @@ from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidge
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock
|
||||
from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
|
||||
|
||||
@@ -35,9 +37,9 @@ class SignalDisplay(BECWidget, QWidget):
|
||||
|
||||
@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)
|
||||
if (dev := self.dev.get(self.device)) is not None:
|
||||
dev.read()
|
||||
dev.read_configuration()
|
||||
|
||||
def _add_refresh_button(self):
|
||||
button_holder = QWidget()
|
||||
@@ -63,11 +65,26 @@ class SignalDisplay(BECWidget, QWidget):
|
||||
self._add_refresh_button()
|
||||
|
||||
if self._device in self.dev:
|
||||
for sig in self.dev[self.device]._info.get("signals", {}).keys():
|
||||
if isinstance(self.dev[self.device], Device):
|
||||
for sig, info in self.dev[self.device]._info.get("signals", {}).items():
|
||||
if info.get("kind_str") in [
|
||||
Kind.hinted.name,
|
||||
Kind.normal.name,
|
||||
Kind.config.name,
|
||||
]:
|
||||
self._content_layout.addWidget(
|
||||
SignalLabel(
|
||||
device=self._device,
|
||||
signal=sig,
|
||||
show_select_button=False,
|
||||
show_default_units=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._content_layout.addWidget(
|
||||
SignalLabel(
|
||||
device=self._device,
|
||||
signal=sig,
|
||||
signal=self._device,
|
||||
show_select_button=False,
|
||||
show_default_units=True,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from bec_lib.device import Device, Signal
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -143,6 +143,14 @@ class SignalLabel(BECWidget, QWidget):
|
||||
"show_default_units.setter",
|
||||
"show_select_button",
|
||||
"show_select_button.setter",
|
||||
"show_hinted_signals",
|
||||
"show_hinted_signals.setter",
|
||||
"show_normal_signals",
|
||||
"show_normal_signals.setter",
|
||||
"show_config_signals",
|
||||
"show_config_signals.setter",
|
||||
"display_array_data",
|
||||
"display_array_data.setter",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -183,8 +191,9 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._dtype = None
|
||||
|
||||
self._show_hinted_signals: bool = True
|
||||
self._show_normal_signals: bool = False
|
||||
self._show_config_signals: bool = False
|
||||
self._show_normal_signals: bool = True
|
||||
self._show_config_signals: bool = True
|
||||
self._display_array_data: bool = False
|
||||
|
||||
self._outer_layout = QHBoxLayout()
|
||||
self._layout = QHBoxLayout()
|
||||
@@ -197,7 +206,7 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._update_label()
|
||||
self._label.setLayout(self._layout)
|
||||
|
||||
self._value: str = ""
|
||||
self._value: Any = ""
|
||||
self._display = QLabel()
|
||||
self._layout.addWidget(self._display)
|
||||
|
||||
@@ -210,6 +219,8 @@ class SignalLabel(BECWidget, QWidget):
|
||||
|
||||
self._select_button.clicked.connect(self.show_choice_dialog)
|
||||
self.get_bec_shortcuts()
|
||||
self._device_obj = self.dev.get(self._device)
|
||||
self._signal_key, self._signal_info = "", {}
|
||||
|
||||
self._connected: bool = False
|
||||
self.connect_device()
|
||||
@@ -226,6 +237,7 @@ class SignalLabel(BECWidget, QWidget):
|
||||
|
||||
@SafeSlot()
|
||||
def _process_dialog(self, device: str, signal: str):
|
||||
signal = signal or device
|
||||
self.disconnect_device()
|
||||
self.device = device
|
||||
self.signal = signal
|
||||
@@ -241,45 +253,34 @@ class SignalLabel(BECWidget, QWidget):
|
||||
def connect_device(self):
|
||||
"""Subscribe to the Redis topic for the device to display"""
|
||||
if not self._connected and self._device and self._device in self.dev:
|
||||
self._connected = True
|
||||
self._read_endpoint = MessageEndpoints.device_read(self._device)
|
||||
self._signal_key, self._signal_info = self._signal_key_and_info()
|
||||
self._manual_read()
|
||||
self._read_endpoint = MessageEndpoints.device_readback(self._device)
|
||||
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._connected = True
|
||||
self.set_display_value(self._value)
|
||||
|
||||
def disconnect_device(self):
|
||||
"""Unsubscribe from the Redis topic for the device to display"""
|
||||
if self._connected:
|
||||
self._connected = False
|
||||
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)
|
||||
self._connected = False
|
||||
|
||||
def _manual_read(self):
|
||||
if self._device is None or not isinstance(
|
||||
(device := self.dev.get(self._device)), Device | Signal
|
||||
):
|
||||
self._units = ""
|
||||
self._value = "__"
|
||||
if not isinstance(self._device_obj, Device | Signal):
|
||||
self._value, self._units = "__", ""
|
||||
return
|
||||
signal, info = (
|
||||
(
|
||||
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
|
||||
signal = None
|
||||
if signal is None:
|
||||
self._units = ""
|
||||
self._value = "__"
|
||||
reading = (self._device_obj.read() or {}) | (self._device_obj.read_configuration() or {})
|
||||
value = reading.get(self._signal_key, {}).get("value")
|
||||
if value is None:
|
||||
self._value, self._units = "__", ""
|
||||
return
|
||||
self._value = signal.get()
|
||||
self._units = info.get("egu", "")
|
||||
self._dtype = info.get("dtype", "float")
|
||||
self._value = value
|
||||
self._units = self._signal_info.get("egu", "")
|
||||
self._dtype = self._signal_info.get("dtype", "float")
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback(self, msg: dict, metadata: dict) -> None:
|
||||
@@ -287,8 +288,7 @@ class SignalLabel(BECWidget, QWidget):
|
||||
Update the display with the new value.
|
||||
"""
|
||||
try:
|
||||
signal_to_read = self._patch_hinted_signal()
|
||||
_value = msg["signals"].get(signal_to_read, {}).get("value")
|
||||
_value = msg["signals"].get(self._signal_key, {}).get("value")
|
||||
if _value is not None:
|
||||
self._value = _value
|
||||
self.set_display_value(self._value)
|
||||
@@ -298,13 +298,19 @@ class SignalLabel(BECWidget, QWidget):
|
||||
f"Error processing incoming reading: {msg}, handled with exception: {''.join(traceback.format_exception(e))}"
|
||||
)
|
||||
|
||||
def _patch_hinted_signal(self):
|
||||
if self.dev[self._device]._info["signals"] == {}:
|
||||
return self._signal
|
||||
signal_info = self.dev[self._device]._info["signals"][self._signal]
|
||||
return (
|
||||
signal_info["obj_name"] if signal_info["kind_str"] == Kind.hinted.name else self._signal
|
||||
)
|
||||
def _signal_key_and_info(self) -> tuple[str, dict]:
|
||||
if isinstance(self._device_obj, Device):
|
||||
try:
|
||||
signal_info = self._device_obj._info["signals"][self._signal]
|
||||
except KeyError:
|
||||
return "", {}
|
||||
if signal_info["kind_str"] == Kind.hinted.name:
|
||||
return signal_info["obj_name"], signal_info
|
||||
else:
|
||||
return f"{self._device}_{self._signal}", signal_info
|
||||
elif isinstance(self._device_obj, Signal):
|
||||
return self._device, self._device_obj._info["describe_configuration"]
|
||||
return "", {}
|
||||
|
||||
@SafeProperty(str)
|
||||
def device(self) -> str:
|
||||
@@ -315,6 +321,7 @@ class SignalLabel(BECWidget, QWidget):
|
||||
def device(self, value: str) -> None:
|
||||
self.disconnect_device()
|
||||
self._device = value
|
||||
self._device_obj = self.dev.get(self._device)
|
||||
self._config.device = value
|
||||
self.connect_device()
|
||||
self._update_label()
|
||||
@@ -382,6 +389,16 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._decimal_places = value
|
||||
self._update_label()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def display_array_data(self) -> bool:
|
||||
"""Displays the full data from array signals if set to True."""
|
||||
return self._display_array_data
|
||||
|
||||
@display_array_data.setter
|
||||
def display_array_data(self, value: bool) -> None:
|
||||
self._display_array_data = value
|
||||
self.set_display_value(self._value)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def show_hinted_signals(self) -> bool:
|
||||
"""In the signal selection menu, show hinted signals"""
|
||||
@@ -409,7 +426,9 @@ class SignalLabel(BECWidget, QWidget):
|
||||
def show_normal_signals(self, value: bool) -> None:
|
||||
self._show_normal_signals = value
|
||||
|
||||
def _format_value(self, value: str):
|
||||
def _format_value(self, value: Any):
|
||||
if self._dtype == "array" and not self.display_array_data:
|
||||
return "ARRAY DATA"
|
||||
if self._decimal_places == 0:
|
||||
return value
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "2.29.0"
|
||||
version = "2.30.1"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_lib.device import Device
|
||||
from qtpy.QtCore import QPoint, Qt
|
||||
from qtpy.QtWidgets import QTabWidget
|
||||
|
||||
@@ -9,6 +10,9 @@ from bec_widgets.widgets.services.device_browser.device_browser import DeviceBro
|
||||
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||
DeviceConfigForm,
|
||||
)
|
||||
from bec_widgets.widgets.services.device_browser.device_item.device_signal_display import (
|
||||
SignalDisplay,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
@@ -142,3 +146,39 @@ def test_device_deletion(device_browser, qtbot):
|
||||
assert widget.device in device_browser._device_items
|
||||
qtbot.mouseClick(widget.delete_button, Qt.LeftButton)
|
||||
qtbot.waitUntil(lambda: widget.device not in device_browser._device_items, timeout=10000)
|
||||
|
||||
|
||||
def test_signal_display(mocked_client, qtbot):
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device")
|
||||
qtbot.addWidget(signal_display)
|
||||
device_mock = mock.MagicMock()
|
||||
signal_display.dev = {"test_device": device_mock}
|
||||
signal_display._refresh()
|
||||
device_mock.read.assert_called()
|
||||
device_mock.read_configuration.assert_called()
|
||||
|
||||
|
||||
def test_signal_display_no_device(mocked_client, qtbot):
|
||||
device_mock = mock.MagicMock()
|
||||
mocked_client.client.device_manager.devices = {"test_device_1": device_mock}
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device_2")
|
||||
qtbot.addWidget(signal_display)
|
||||
assert (
|
||||
signal_display._content_layout.itemAt(1).widget().text()
|
||||
== "Device test_device_2 not found in device manager!"
|
||||
)
|
||||
signal_display._refresh()
|
||||
device_mock.read.assert_not_called()
|
||||
device_mock.read_configuration.assert_not_called()
|
||||
|
||||
|
||||
def test_signal_display_omitted_not_added(mocked_client, qtbot):
|
||||
device_mock = mock.MagicMock(spec=Device)
|
||||
device_mock._info = {"signals": {"signal_1": {"kind_str": "omitted"}}}
|
||||
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device_1")
|
||||
signal_display.dev = {"test_device_1": device_mock}
|
||||
signal_display._populate()
|
||||
|
||||
qtbot.addWidget(signal_display)
|
||||
assert signal_display._content_layout.itemAt(1).widget() is None
|
||||
|
||||
@@ -84,7 +84,15 @@ def test_initialization(signal_label: SignalLabel):
|
||||
|
||||
|
||||
def test_initialization_with_device(qtbot, mocked_client: MagicMock):
|
||||
with patch.object(mocked_client.device_manager.devices.samx, "_info", SAMX_INFO_DICT):
|
||||
|
||||
with (
|
||||
patch.object(mocked_client.device_manager.devices.samx, "_info", SAMX_INFO_DICT),
|
||||
patch.object(
|
||||
mocked_client.device_manager.devices.samx,
|
||||
"_get_root_recursively",
|
||||
lambda *_: (MagicMock(),),
|
||||
),
|
||||
):
|
||||
widget = SignalLabel(device="samx", signal="readback", client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
@@ -188,6 +196,8 @@ def test_choice_dialog_with_no_client(qtbot):
|
||||
|
||||
|
||||
def test_dialog_has_signals(signal_label: SignalLabel, qtbot):
|
||||
signal_label.show_config_signals = False
|
||||
signal_label.show_normal_signals = False
|
||||
signal_label._process_dialog = MagicMock()
|
||||
dialog = signal_label.show_choice_dialog()
|
||||
qtbot.waitUntil(dialog.button_box.button(QDialogButtonBox.Ok).isVisible, timeout=500)
|
||||
|
||||
Reference in New Issue
Block a user