From 7f0098f1533d419cc75801c4d6cbea485c7bbf94 Mon Sep 17 00:00:00 2001 From: David Perl Date: Thu, 17 Jul 2025 15:27:20 +0200 Subject: [PATCH] feat: save and load config from devicebrowser --- .../services/device_browser/device_browser.py | 47 +++++++++++++++++-- .../services/device_browser/device_browser.ui | 14 ++++++ .../device_item/config_communicator.py | 3 +- .../device_item/device_config_dialog.py | 11 ++--- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/bec_widgets/widgets/services/device_browser/device_browser.py b/bec_widgets/widgets/services/device_browser/device_browser.py index 1a1f647c..d03ee93d 100644 --- a/bec_widgets/widgets/services/device_browser/device_browser.py +++ b/bec_widgets/widgets/services/device_browser/device_browser.py @@ -1,7 +1,9 @@ import os import re from functools import partial +from typing import Callable +import bec_lib from bec_lib.callback_handler import EventType from bec_lib.config_helper import ConfigHelper from bec_lib.endpoints import MessageEndpoints @@ -10,7 +12,14 @@ from bec_lib.messages import ConfigAction, ScanStatusMessage from bec_qthemes import material_icon from pyqtgraph import SignalProxy from qtpy.QtCore import QSize, QThreadPool, Signal -from qtpy.QtWidgets import QLabel, QListWidget, QListWidgetItem, QToolButton, QVBoxLayout, QWidget +from qtpy.QtWidgets import ( + QFileDialog, + QListWidget, + QListWidgetItem, + QToolButton, + QVBoxLayout, + QWidget, +) from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils.bec_widget import BECWidget @@ -61,12 +70,14 @@ class DeviceBrowser(BECWidget, QWidget): self.bec_dispatcher.client.callbacks.register( EventType.SCAN_STATUS, self.scan_status_changed ) + self._default_config_dir = os.path.abspath( + os.path.join(os.path.dirname(bec_lib.__file__), "./configs/") + ) self.devices_changed.connect(self.update_device_list) - self.ui.add_button.clicked.connect(self._create_add_dialog) - self.ui.add_button.setIcon(material_icon("add", size=(20, 20), convert_to_pixmap=False)) self.init_warning_label() + self.init_tool_buttons() self.init_device_list() self.update_device_list() @@ -90,6 +101,18 @@ class DeviceBrowser(BECWidget, QWidget): initial_status = scan_status.status if scan_status is not None else "closed" self.set_editing_mode(initial_status not in ["open", "paused"]) + def init_tool_buttons(self): + def _setup_button(button: QToolButton, icon: str, slot: Callable, tooltip: str = ""): + button.clicked.connect(slot) + button.setIcon(material_icon(icon, size=(20, 20), convert_to_pixmap=False)) + button.setToolTip(tooltip) + + _setup_button(self.ui.add_button, "add", self._create_add_dialog, "add new device") + _setup_button(self.ui.save_button, "save", self._save_to_file, "save config to file") + _setup_button( + self.ui.import_button, "input", self._load_from_file, "append/merge config from file" + ) + def _create_add_dialog(self): dialog = DeviceConfigDialog(parent=self, device=None, action="add") dialog.open() @@ -148,7 +171,7 @@ class DeviceBrowser(BECWidget, QWidget): self.dev_list.addItem(item) self._device_items[device] = item - @SafeSlot(bool) + @SafeSlot(dict, dict) def scan_status_changed(self, scan_info: dict, _: dict): """disable editing when scans are running and enable editing when they are finished""" msg = ScanStatusMessage.model_validate(scan_info) @@ -190,6 +213,22 @@ class DeviceBrowser(BECWidget, QWidget): for device in self.dev: self._device_items[device].setHidden(not self.regex.search(device)) + @SafeSlot() + def _load_from_file(self): + file_path, _ = QFileDialog.getOpenFileName( + self, "Update config from file", self._default_config_dir, "Config files (*.yml *.yaml)" + ) + if file_path: + self._config_helper.update_session_with_file(file_path) + + @SafeSlot() + def _save_to_file(self): + file_path, _ = QFileDialog.getSaveFileName( + self, "Save config to file", self._default_config_dir, "Config files (*.yml *.yaml)" + ) + if file_path: + self._config_helper.save_current_session(file_path) + if __name__ == "__main__": # pragma: no cover import sys diff --git a/bec_widgets/widgets/services/device_browser/device_browser.ui b/bec_widgets/widgets/services/device_browser/device_browser.ui index 8f53f709..9a2d4ce2 100644 --- a/bec_widgets/widgets/services/device_browser/device_browser.ui +++ b/bec_widgets/widgets/services/device_browser/device_browser.ui @@ -51,6 +51,20 @@ + + + + ... + + + + + + + ... + + + diff --git a/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py b/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py index 2326734a..4a469dbb 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py +++ b/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py @@ -39,9 +39,10 @@ class CommunicateConfigAction(QRunnable): raise ValueError( "Must be updating a device or be supplied a name for a new device" ) - if "deviceConfig" not in self.config: + if "deviceConfig" not in self.config or self.action in ["add", "remove"]: self.process_simple_action(dev_name) else: + # updating an existing device, but need to recreate it for this change self.process_remove_readd(dev_name) else: raise ValueError(f"action {self.action} is not supported") diff --git a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py index c9b594c7..f952f2c1 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py +++ b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py @@ -68,7 +68,7 @@ class DeviceConfigDialog(BECWidget, QDialog): self.client.connector, self.client._service_name ) self._device = device - self._action = action + self._action: Literal["update", "add"] = action self._q_threadpool = threadpool or QThreadPool() self.setWindowTitle(f"Edit config for: {device}") self._container = QStackedLayout() @@ -168,10 +168,9 @@ class DeviceConfigDialog(BECWidget, QDialog): diff = { k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k) } - if self._initial_config["deviceConfig"] in [{}, None] and new_config["deviceConfig"] in [ - {}, - None, - ]: + if self._initial_config.get("deviceConfig") in [{}, None] and new_config.get( + "deviceConfig" + ) in [{}, None]: diff.pop("deviceConfig", None) if diff.get("deviceConfig") is not None: # TODO: special cased in some parts of device manager but not others, should @@ -222,7 +221,7 @@ class DeviceConfigDialog(BECWidget, QDialog): self._proc_device_config_change(updated_config) def _proc_device_config_change(self, config: dict): - logger.info(f"Sending request to update device config: {config}") + logger.info(f"Sending request to {self._action} device config: {config}") self._start_waiting_display() communicate_update = CommunicateConfigAction(