0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

fix: monitor.py replots last scan after changing config with new signals; config_dialog.py checks if the new config is valid with BEC

This commit is contained in:
wyzula-jan
2024-01-23 14:02:19 +01:00
parent d211b47f4c
commit ab275b8e5f
3 changed files with 142 additions and 7 deletions

View File

@ -13,6 +13,11 @@ from qtpy.QtWidgets import (
) )
from bec_widgets.utils.yaml_dialog import load_yaml, save_yaml from bec_widgets.utils.yaml_dialog import load_yaml, save_yaml
from bec_widgets.validation import MonitorConfigValidator
from pydantic import ValidationError
from qtpy.QtWidgets import QApplication, QMessageBox
from bec_widgets.utils.bec_dispatcher import BECDispatcher
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
Ui_Form, BaseClass = uic.loadUiType(os.path.join(current_path, "config_dialog.ui")) Ui_Form, BaseClass = uic.loadUiType(os.path.join(current_path, "config_dialog.ui"))
@ -168,10 +173,25 @@ CONFIG_SCAN_MODE = {
class ConfigDialog(QWidget, Ui_Form): class ConfigDialog(QWidget, Ui_Form):
config_updated = pyqtSignal(dict) config_updated = pyqtSignal(dict)
def __init__(self, default_config=None): def __init__(
self,
client=None,
default_config=None,
skip_validation: bool = False,
):
super(ConfigDialog, self).__init__() super(ConfigDialog, self).__init__()
self.setupUi(self) self.setupUi(self)
# Client
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
# Init validator
self.skip_validation = skip_validation
if self.skip_validation is False:
self.validator = MonitorConfigValidator(self.dev)
# Connect the Ok/Apply/Cancel buttons # Connect the Ok/Apply/Cancel buttons
self.pushButton_ok.clicked.connect(self.apply_and_close) self.pushButton_ok.clicked.connect(self.apply_and_close)
self.pushButton_apply.clicked.connect(self.apply_config) self.pushButton_apply.clicked.connect(self.apply_config)
@ -522,8 +542,45 @@ class ConfigDialog(QWidget, Ui_Form):
def apply_and_close(self): def apply_and_close(self):
new_config = self.apply_config() new_config = self.apply_config()
self.config_updated.emit(new_config) if self.skip_validation is True:
self.close() self.config_updated.emit(new_config)
self.close()
else:
try:
validated_config = self.validator.validate_monitor_config(new_config)
approved_config = validated_config.model_dump()
self.config_updated.emit(approved_config)
self.close()
except ValidationError as e:
error_str = str(e)
formatted_error_message = ConfigDialog.format_validation_error(error_str)
# Display the formatted error message in a popup
QMessageBox.critical(self, "Configuration Error", formatted_error_message)
@staticmethod
def format_validation_error(error_str: str) -> str:
"""
Format the validation error string to be displayed in a popup.
Args:
error_str(str): Error string from the validation error.
"""
error_lines = error_str.split("\n")
# The first line contains the number of errors.
error_header = f"<p><b>{error_lines[0]}</b></p><hr>"
formatted_error_message = error_header
# Skip the first line as it's the header.
error_details = error_lines[1:]
# Iterate through pairs of lines (each error's two lines).
for i in range(0, len(error_details), 2):
location = error_details[i]
message = error_details[i + 1] if i + 1 < len(error_details) else ""
formatted_error_message += f"<p><b>{location}</b><br>{message}</p><hr>"
return formatted_error_message
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover

View File

@ -309,7 +309,7 @@ class BECMonitor(pg.GraphicsLayoutWidget):
self.enable_crosshair = enable_crosshair self.enable_crosshair = enable_crosshair
# Displayed Data # Displayed Data
self.database = {} self.database = None
self.crosshairs = None self.crosshairs = None
self.plots = None self.plots = None
@ -352,6 +352,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
# Initialize the UI # Initialize the UI
self._init_ui(self.plot_settings["num_columns"]) self._init_ui(self.plot_settings["num_columns"])
if self.scanID is not None:
self.replot_last_scan()
def _init_database(self, plot_data_config: dict, source_type_to_init=None) -> dict: def _init_database(self, plot_data_config: dict, source_type_to_init=None) -> dict:
""" """
Initializes or updates the database for the PlotApp. Initializes or updates the database for the PlotApp.
@ -601,7 +604,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
"""Show the configuration dialog.""" """Show the configuration dialog."""
from bec_widgets.widgets import ConfigDialog from bec_widgets.widgets import ConfigDialog
dialog = ConfigDialog(default_config=self.config) dialog = ConfigDialog(
client=self.client, default_config=self.config, skip_validation=self.skip_validation
)
dialog.config_updated.connect(self.on_config_update) dialog.config_updated.connect(self.on_config_update)
dialog.show() dialog.show()
@ -772,6 +777,13 @@ class BECMonitor(pg.GraphicsLayoutWidget):
else: else:
print(f"No data found for {device_name} {entry}") print(f"No data found for {device_name} {entry}")
def replot_last_scan(self):
"""
Replot the last scan.
"""
self.scan_segment_update()
self.update_plot(source_type="scan_segment")
@pyqtSlot(dict) @pyqtSlot(dict)
def on_data_from_redis(self, msg) -> None: def on_data_from_redis(self, msg) -> None:
""" """

View File

@ -1,4 +1,6 @@
import os import os
from unittest.mock import MagicMock
import yaml import yaml
import pytest import pytest
@ -15,9 +17,73 @@ def load_test_config(config_name):
return config return config
class FakeDevice:
"""Fake minimal positioner class for testing."""
def __init__(self, name, enabled=True):
self.name = name
self.enabled = enabled
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name}}
def __contains__(self, item):
return item == self.name
@property
def _hints(self):
return [self.name]
def set_value(self, fake_value: float = 1.0) -> None:
"""
Setup fake value for device readout
Args:
fake_value(float): Desired fake value
"""
self.signals[self.name]["value"] = fake_value
def describe(self) -> dict:
"""
Get the description of the device
Returns:
dict: Description of the device
"""
return self.description
def get_mocked_device(device_name: str):
"""
Helper function to mock the devices
Args:
device_name(str): Name of the device to mock
"""
return FakeDevice(name=device_name, enabled=True)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def config_dialog(qtbot): def mocked_client():
widget = ConfigDialog() # Create a dictionary of mocked devices
device_names = ["samx", "gauss_bpm", "gauss_adc1", "gauss_adc2", "gauss_adc3", "bpm4i"]
mocked_devices = {name: get_mocked_device(name) for name in device_names}
# Create a MagicMock object
client = MagicMock()
# Mock the device_manager.devices attribute
client.device_manager.devices = MagicMock()
client.device_manager.devices.__getitem__.side_effect = lambda x: mocked_devices.get(x)
client.device_manager.devices.__contains__.side_effect = lambda x: x in mocked_devices
# Set each device as an attribute of the mock
for name, device in mocked_devices.items():
setattr(client.device_manager.devices, name, device)
return client
@pytest.fixture(scope="function")
def config_dialog(qtbot, mocked_client):
client = mocked_client
widget = ConfigDialog(client=client)
qtbot.addWidget(widget) qtbot.addWidget(widget)
qtbot.waitExposed(widget) qtbot.waitExposed(widget)
yield widget yield widget