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

feat!: add support for auto updates

This commit is contained in:
2025-04-09 12:34:15 +02:00
committed by wyzula-jan
parent 99383b7715
commit 2511056557
3 changed files with 352 additions and 187 deletions

View File

@ -1,169 +1,295 @@
# TODO autoupdate disabled from __future__ import annotations
# from __future__ import annotations
# from typing import TYPE_CHECKING, Literal, overload
# import threading
# from queue import Queue from bec_lib.endpoints import MessageEndpoints
# from typing import TYPE_CHECKING from bec_lib.logger import bec_logger
# from bec_lib.messages import ScanStatusMessage
# from pydantic import BaseModel
# from bec_widgets.utils.error_popups import SafeSlot
# if TYPE_CHECKING:
# from .client import BECDockArea, BECFigure if TYPE_CHECKING: # pragma: no cover
# from bec_widgets.utils.bec_widget import BECWidget
# from bec_widgets.widgets.containers.dock.dock import BECDock
# class ScanInfo(BaseModel): from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
# scan_id: str from bec_widgets.widgets.plots.image.image import Image
# scan_number: int from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
# scan_name: str from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
# scan_report_devices: list from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
# monitored_devices: list from bec_widgets.widgets.plots.waveform.waveform import Waveform
# status: str
# model_config: dict = {"validate_assignment": True}
# logger = bec_logger.logger
#
# class AutoUpdates:
# create_default_dock: bool = False class AutoUpdates:
# enabled: bool = False _default_dock: BECDock
# dock_name: str = None
# def __init__(self, dock_area: BECDockArea):
# def __init__(self, gui: BECDockArea): self.dock_area = dock_area
# self.gui = gui self.bec_dispatcher = dock_area.bec_dispatcher
# self._default_dock = None self._default_dock = None # type:ignore
# self._default_fig = None self.current_widget: BECWidget | None = None
# self.dock_name = None
# def start_default_dock(self): self._enabled = False
# """
# Create a default dock for the auto updates. def connect(self):
# """ """
# self.dock_name = "default_figure" Establish all connections for the auto updates.
# self._default_dock = self.gui.new(self.dock_name) """
# self._default_dock.new("BECFigure") self.bec_dispatcher.connect_slot(self._on_scan_status, MessageEndpoints.scan_status())
# self._default_fig = self._default_dock.elements_list[0]
# def disconnect(self):
# @staticmethod """
# def get_scan_info(msg) -> ScanInfo: Disconnect all connections for the auto updates.
# """ """
# Update the script with the given data. self.bec_dispatcher.disconnect_slot(
# """ self._on_scan_status, MessageEndpoints.scan_status() # type:ignore
# info = msg.info )
# status = msg.status
# scan_id = msg.scan_id @SafeSlot()
# scan_number = info.get("scan_number", 0) def _on_scan_status(self, content: dict, metadata: dict) -> None:
# scan_name = info.get("scan_name", "Unknown") """
# scan_report_devices = info.get("scan_report_devices", []) Callback for scan status messages.
# monitored_devices = info.get("readout_priority", {}).get("monitored", []) """
# monitored_devices = [dev for dev in monitored_devices if dev not in scan_report_devices] msg = ScanStatusMessage(**content, metadata=metadata)
# return ScanInfo(
# scan_id=scan_id, match msg.status:
# scan_number=scan_number, case "open":
# scan_name=scan_name, self.on_scan_open(msg)
# scan_report_devices=scan_report_devices, case "closed":
# monitored_devices=monitored_devices, self.on_scan_closed(msg)
# status=status, case ["aborted", "halted"]:
# ) self.on_scan_abort(msg)
# case _:
# def get_default_figure(self) -> BECFigure | None: pass
# """
# Get the default figure from the GUI. def start_default_dock(self):
# """ """
# return self._default_fig Create a default dock for the auto updates.
# """
# def do_update(self, msg): self.dock_name = "update_dock"
# """ self._default_dock = self.dock_area.new(self.dock_name)
# Run the update function if enabled. self.current_widget = self._default_dock.new("Waveform")
# """
# if not self.enabled: @overload
# return def set_dock_to_widget(self, widget: Literal["Waveform"]) -> Waveform: ...
# if msg.status != "open":
# return @overload
# info = self.get_scan_info(msg) def set_dock_to_widget(self, widget: Literal["Image"]) -> Image: ...
# return self.handler(info)
# @overload
# def get_selected_device(self, monitored_devices, selected_device): def set_dock_to_widget(self, widget: Literal["ScatterWaveform"]) -> ScatterWaveform: ...
# """
# Get the selected device for the plot. If no device is selected, the first @overload
# device in the monitored devices list is selected. def set_dock_to_widget(self, widget: Literal["MotorMap"]) -> MotorMap: ...
# """
# if selected_device: @overload
# return selected_device def set_dock_to_widget(self, widget: Literal["MultiWaveform"]) -> MultiWaveform: ...
# if len(monitored_devices) > 0:
# sel_device = monitored_devices[0] def set_dock_to_widget(
# return sel_device self,
# return None widget: Literal["Waveform", "Image", "ScatterWaveform", "MotorMap", "MultiWaveForm"] | str,
# ) -> BECWidget:
# def handler(self, info: ScanInfo) -> None: """
# """ Set the dock to the widget.
# Default update function.
# """ Args:
# if info.scan_name == "line_scan" and info.scan_report_devices: widget (str): The widget to set the dock to. Must be the name of a valid widget class.
# return self.simple_line_scan(info)
# if info.scan_name == "grid_scan" and info.scan_report_devices: Returns:
# return self.simple_grid_scan(info) BECWidget: The widget that was set.
# if info.scan_report_devices: """
# return self.best_effort(info) if self._default_dock is None or self.current_widget is None:
# logger.warning(
# def simple_line_scan(self, info: ScanInfo) -> None: f"Auto Updates: No default dock found. Creating a new one with name {self.dock_name}"
# """ )
# Simple line scan. self.start_default_dock()
# """ assert self.current_widget is not None
# fig = self.get_default_figure()
# if not fig: if not self.current_widget.__class__.__name__ == widget:
# return self._default_dock.delete(self.current_widget.object_name)
# dev_x = info.scan_report_devices[0] self.current_widget = self._default_dock.new(widget)
# selected_device = yield self.gui.selected_device return self.current_widget
# dev_y = self.get_selected_device(info.monitored_devices, selected_device)
# if not dev_y: def get_selected_device(
# return self, monitored_devices, selected_device: str | None = None
# yield fig.clear_all() ) -> str | None:
# yield fig.plot( """
# x_name=dev_x, Get the selected device for the plot. If no device is selected, the first
# y_name=dev_y, device in the monitored devices list is selected.
# label=f"Scan {info.scan_number} - {dev_y}", """
# title=f"Scan {info.scan_number}",
# x_label=dev_x, if selected_device is None:
# y_label=dev_y, selected_device = self.dock_area.selected_device
# ) if selected_device:
# return selected_device
# def simple_grid_scan(self, info: ScanInfo) -> None: if len(monitored_devices) > 0:
# """ sel_device = monitored_devices[0]
# Simple grid scan. return sel_device
# """ return None
# fig = self.get_default_figure()
# if not fig: @property
# return def enabled(self) -> bool:
# dev_x = info.scan_report_devices[0] """
# dev_y = info.scan_report_devices[1] Get the enabled status of the auto updates.
# selected_device = yield self.gui.selected_device """
# dev_z = self.get_selected_device(info.monitored_devices, selected_device) return self._enabled
# yield fig.clear_all()
# yield fig.plot( @enabled.setter
# x_name=dev_x, def enabled(self, value: bool) -> None:
# y_name=dev_y, """
# z_name=dev_z, Set the enabled status of the auto updates.
# label=f"Scan {info.scan_number} - {dev_z}", """
# title=f"Scan {info.scan_number}", if self._enabled == value:
# x_label=dev_x, return
# y_label=dev_y, self._enabled = value
# )
# if value:
# def best_effort(self, info: ScanInfo) -> None: self.connect()
# """ self.on_start()
# Best effort scan. else:
# """ self.disconnect()
# fig = self.get_default_figure() self.on_stop()
# if not fig:
# return ########################################################################
# dev_x = info.scan_report_devices[0] ################# Update Functions #####################################
# selected_device = yield self.gui.selected_device ########################################################################
# dev_y = self.get_selected_device(info.monitored_devices, selected_device)
# if not dev_y: def simple_line_scan(self, info: ScanStatusMessage) -> None:
# return """
# yield fig.clear_all() Simple line scan.
# yield fig.plot(
# x_name=dev_x, Args:
# y_name=dev_y, info (ScanStatusMessage): The scan status message.
# label=f"Scan {info.scan_number} - {dev_y}", """
# title=f"Scan {info.scan_number}",
# x_label=dev_x, # Set the dock to the waveform widget
# y_label=dev_y, wf = self.set_dock_to_widget("Waveform")
# )
# Get the scan report devices reported by the scan
dev_x = info.scan_report_devices[0] # type:ignore
# For the y axis, get the selected device
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
if not dev_y:
return
# Clear the waveform widget and plot the data
# with the scan number and device names
# as the label and title
wf.clear_all()
wf.plot(
x_name=dev_x,
y_name=dev_y,
label=f"Scan {info.scan_number} - {dev_y}",
title=f"Scan {info.scan_number}",
x_label=dev_x,
y_label=dev_y,
)
logger.info(
f"Auto Update [simple_line_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}"
)
def simple_grid_scan(self, info: ScanStatusMessage) -> None:
"""
Simple grid scan.
Args:
info (ScanStatusMessage): The scan status message.
"""
# Set the dock to the scatter waveform widget
scatter = self.set_dock_to_widget("ScatterWaveform")
# Get the scan report devices reported by the scan
dev_x, dev_y = info.scan_report_devices[0], info.scan_report_devices[1] # type:ignore
dev_z = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
# Clear the scatter waveform widget and plot the data
scatter.clear_all()
scatter.plot(
x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
)
logger.info(
f"Auto Update [simple_grid_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}, z_name={dev_z}"
)
def best_effort(self, info: ScanStatusMessage) -> None:
"""
Best effort scan.
Args:
info (ScanStatusMessage): The scan status message.
"""
# If the scan report devices are empty, there is nothing we can do
if not info.scan_report_devices:
return
dev_x = info.scan_report_devices[0] # type:ignore
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
if not dev_y:
return
# Set the dock to the waveform widget
wf = self.set_dock_to_widget("Waveform")
# Clear the waveform widget and plot the data
wf.clear_all()
wf.plot(
x_name=dev_x,
y_name=dev_y,
label=f"Scan {info.scan_number} - {dev_y}",
title=f"Scan {info.scan_number}",
x_label=dev_x,
y_label=dev_y,
)
logger.info(f"Auto Update [best_effort]: Started plot with: x_name={dev_x}, y_name={dev_y}")
#######################################################################
################# GUI Callbacks #######################################
#######################################################################
def on_start(self) -> None:
"""
Procedure to run when the auto updates are enabled.
"""
self.start_default_dock()
def on_stop(self) -> None:
"""
Procedure to run when the auto updates are disabled.
"""
def on_scan_open(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan starts.
Args:
msg (ScanStatusMessage): The scan status message.
"""
if msg.scan_name == "line_scan" and msg.scan_report_devices:
return self.simple_line_scan(msg)
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
return self.simple_grid_scan(msg)
if msg.scan_report_devices:
return self.best_effort(msg)
return None
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan ends.
Args:
msg (ScanStatusMessage): The scan status message.
"""
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan is aborted.
Args:
msg (ScanStatusMessage): The scan status message.
"""

View File

@ -363,13 +363,6 @@ class BECDockArea(RPCBase):
Return all floating docks to the dock area. Return all floating docks to the dock area.
""" """
@property
@rpc_call
def selected_device(self) -> "str":
"""
None
"""
@rpc_call @rpc_call
def save_state(self) -> "dict": def save_state(self) -> "dict":
""" """
@ -379,6 +372,26 @@ class BECDockArea(RPCBase):
dict: The state of the dock area. dict: The state of the dock area.
""" """
@property
@rpc_call
def selected_device(self) -> "str | None":
"""
Get the selected device from the auto update config.
Returns:
str: The selected device. If no device is selected, None is returned.
"""
@selected_device.setter
@rpc_call
def selected_device(self) -> "str | None":
"""
Get the selected device from the auto update config.
Returns:
str: The selected device. If no device is selected, None is returned.
"""
@rpc_call @rpc_call
def restore_state( def restore_state(
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom" self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal, Optional from typing import TYPE_CHECKING, Literal, Optional
from weakref import WeakValueDictionary from weakref import WeakValueDictionary
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
@ -37,6 +37,9 @@ from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatus
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.cli.auto_updates import AutoUpdates
logger = bec_logger.logger logger = bec_logger.logger
@ -63,8 +66,9 @@ class BECDockArea(BECWidget, QWidget):
"remove", "remove",
"detach_dock", "detach_dock",
"attach_all", "attach_all",
"selected_device",
"save_state", "save_state",
"selected_device",
"selected_device.setter",
"restore_state", "restore_state",
] ]
@ -96,6 +100,8 @@ class BECDockArea(BECWidget, QWidget):
self.layout.setSpacing(5) self.layout.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
self.auto_update: AutoUpdates | None = None
self._auto_update_selected_device: str | None = None
self._instructions_visible = True self._instructions_visible = True
self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True) self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True)
@ -253,15 +259,33 @@ class BECDockArea(BECWidget, QWidget):
) )
@property @property
def selected_device(self) -> str: def selected_device(self) -> str | None:
gui_id = QApplication.instance().gui_id """
auto_update_config = self.client.connector.get( Get the selected device from the auto update config.
MessageEndpoints.gui_auto_update_config(gui_id)
) Returns:
try: str: The selected device. If no device is selected, None is returned.
return auto_update_config.selected_device """
except AttributeError: return self._auto_update_selected_device
return None
@selected_device.setter
def selected_device(self, value: str | None) -> None:
"""
Set the selected device in the auto update config.
Args:
value(str): The selected device.
"""
self._auto_update_selected_device = value
def set_auto_update(self, auto_update_cls: type[AutoUpdates]) -> None:
"""
Set the auto update object for the dock area.
Args:
auto_update(AutoUpdates): The auto update object.
"""
self.auto_update = auto_update_cls(self)
@property @property
def panels(self) -> dict[str, BECDock]: def panels(self) -> dict[str, BECDock]:
@ -447,6 +471,8 @@ class BECDockArea(BECWidget, QWidget):
""" """
Cleanup the dock area. Cleanup the dock area.
""" """
if self.auto_update:
self.auto_update.enabled = False
self.delete_all() self.delete_all()
self.toolbar.close() self.toolbar.close()
self.toolbar.deleteLater() self.toolbar.deleteLater()