mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
feat: add rpc broadcast
This commit is contained in:
@ -9,17 +9,20 @@ import os
|
|||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
import bec_widgets.cli.client as client
|
import bec_widgets.cli.client as client
|
||||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
|
||||||
from bec_widgets.cli.rpc.rpc_base import RPCBase
|
# from bec_widgets.cli.auto_updates import AutoUpdates
|
||||||
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bec_lib import messages
|
from bec_lib import messages
|
||||||
@ -166,7 +169,7 @@ class WidgetNameSpace:
|
|||||||
docs = docs if docs else "No description available"
|
docs = docs if docs else "No description available"
|
||||||
table.add_row(attr, docs)
|
table.add_row(attr, docs)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
return f""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class AvailableWidgetsNamespace:
|
class AvailableWidgetsNamespace:
|
||||||
@ -189,22 +192,13 @@ class AvailableWidgetsNamespace:
|
|||||||
docs = docs if docs else "No description available"
|
docs = docs if docs else "No description available"
|
||||||
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
|
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
|
||||||
console.print(table)
|
console.print(table)
|
||||||
return "" # f"<{self.__class__.__name__}>"
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class BECDockArea(client.BECDockArea):
|
|
||||||
"""Extend the BECDockArea class and add namespaces to access widgets of docks."""
|
|
||||||
|
|
||||||
def __init__(self, gui_id=None, config=None, name=None, parent=None):
|
|
||||||
super().__init__(gui_id, config, name, parent)
|
|
||||||
# Add namespaces for DockArea
|
|
||||||
self.elements = WidgetNameSpace()
|
|
||||||
|
|
||||||
|
|
||||||
class BECGuiClient(RPCBase):
|
class BECGuiClient(RPCBase):
|
||||||
"""BEC GUI client class. Container for GUI applications within Python."""
|
"""BEC GUI client class. Container for GUI applications within Python."""
|
||||||
|
|
||||||
_top_level: dict[str, BECDockArea] = {}
|
_top_level: dict[str, client.BECDockArea] = {}
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@ -217,6 +211,32 @@ class BECGuiClient(RPCBase):
|
|||||||
self._gui_started_event = threading.Event()
|
self._gui_started_event = threading.Event()
|
||||||
self._process = None
|
self._process = None
|
||||||
self._process_output_processing_thread = None
|
self._process_output_processing_thread = None
|
||||||
|
self._exposed_dock_areas = []
|
||||||
|
self._registry_state = {}
|
||||||
|
self._ipython_registry = {}
|
||||||
|
self.available_widgets = AvailableWidgetsNamespace()
|
||||||
|
|
||||||
|
####################
|
||||||
|
#### Client API ####
|
||||||
|
####################
|
||||||
|
|
||||||
|
def connect_to_gui_server(self, gui_id: str) -> None:
|
||||||
|
"""Connect to a GUI server"""
|
||||||
|
# Unregister the old callback
|
||||||
|
self._client.connector.unregister(
|
||||||
|
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||||
|
)
|
||||||
|
self._gui_id = gui_id
|
||||||
|
# Get the registry state
|
||||||
|
msgs = self._client.connector.xread(
|
||||||
|
MessageEndpoints.gui_registry_state(self._gui_id), count=1
|
||||||
|
)
|
||||||
|
if msgs:
|
||||||
|
self._handle_registry_update(msgs[0])
|
||||||
|
# Register the new callback
|
||||||
|
self._client.connector.register(
|
||||||
|
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def windows(self) -> dict:
|
def windows(self) -> dict:
|
||||||
@ -228,6 +248,282 @@ class BECGuiClient(RPCBase):
|
|||||||
"""List with dock areas in the GUI."""
|
"""List with dock areas in the GUI."""
|
||||||
return list(self._top_level.values())
|
return list(self._top_level.values())
|
||||||
|
|
||||||
|
def start(self, wait: bool = False) -> None:
|
||||||
|
"""Start the GUI server."""
|
||||||
|
return self._start(wait=wait)
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Show the GUI window."""
|
||||||
|
if self._process is not None:
|
||||||
|
return self._show_all()
|
||||||
|
# backward compatibility: show() was also starting server
|
||||||
|
return self._start_server(wait=True)
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
"""Hide the GUI window."""
|
||||||
|
return self._hide_all()
|
||||||
|
|
||||||
|
def new(
|
||||||
|
self,
|
||||||
|
name: str | None = None,
|
||||||
|
wait: bool = True,
|
||||||
|
geometry: tuple[int, int, int, int] | None = None,
|
||||||
|
) -> client.BECDockArea:
|
||||||
|
"""Create a new top-level dock area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str, optional): The name of the dock area. Defaults to None.
|
||||||
|
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
|
||||||
|
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
|
||||||
|
Returns:
|
||||||
|
client.BECDockArea: The new dock area.
|
||||||
|
"""
|
||||||
|
if len(self.window_list) == 0:
|
||||||
|
self.show()
|
||||||
|
if wait:
|
||||||
|
with wait_for_server(self):
|
||||||
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||||
|
widget = rpc_client._run_rpc(
|
||||||
|
"new_dock_area", name, geometry
|
||||||
|
) # pylint: disable=protected-access
|
||||||
|
self._top_level[widget.widget_name] = widget
|
||||||
|
return widget
|
||||||
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||||
|
widget = rpc_client._run_rpc(
|
||||||
|
"new_dock_area", name, geometry
|
||||||
|
) # pylint: disable=protected-access
|
||||||
|
self._top_level[widget.widget_name] = widget
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def delete(self, name: str) -> None:
|
||||||
|
"""Delete a dock area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): The name of the dock area.
|
||||||
|
"""
|
||||||
|
widget = self.windows.get(name)
|
||||||
|
if widget is None:
|
||||||
|
raise ValueError(f"Dock area {name} not found.")
|
||||||
|
widget._run_rpc("close") # pylint: disable=protected-access
|
||||||
|
|
||||||
|
def delete_all(self) -> None:
|
||||||
|
"""Delete all dock areas."""
|
||||||
|
for widget_name in self.windows:
|
||||||
|
self.delete(widget_name)
|
||||||
|
|
||||||
|
def kill_server(self) -> None:
|
||||||
|
"""Kill the GUI server."""
|
||||||
|
self._top_level.clear()
|
||||||
|
self._killed = True
|
||||||
|
|
||||||
|
if self._gui_started_timer is not None:
|
||||||
|
self._gui_started_timer.cancel()
|
||||||
|
self._gui_started_timer.join()
|
||||||
|
|
||||||
|
if self._process is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._process:
|
||||||
|
logger.success("Stopping GUI...")
|
||||||
|
self._process.terminate()
|
||||||
|
if self._process_output_processing_thread:
|
||||||
|
self._process_output_processing_thread.join()
|
||||||
|
self._process.wait()
|
||||||
|
self._process = None
|
||||||
|
|
||||||
|
# Unregister the registry state
|
||||||
|
self._client.connector.unregister(
|
||||||
|
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Deprecated. Use kill_server() instead."""
|
||||||
|
# FIXME, deprecated in favor of kill, will be removed in the future
|
||||||
|
self.kill_server()
|
||||||
|
|
||||||
|
#########################
|
||||||
|
#### Private methods ####
|
||||||
|
#########################
|
||||||
|
|
||||||
|
def _gui_post_startup(self):
|
||||||
|
timeout = 10
|
||||||
|
while time.time() < time.time() + timeout:
|
||||||
|
if len(list(self._registry_state.keys())) == 0:
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# FIXME AUTO UPDATES
|
||||||
|
# if self._auto_updates_enabled:
|
||||||
|
# if self._auto_updates is None:
|
||||||
|
# auto_updates = self._get_update_script()
|
||||||
|
# if auto_updates is None:
|
||||||
|
# AutoUpdates.create_default_dock = True
|
||||||
|
# AutoUpdates.enabled = True
|
||||||
|
# auto_updates = AutoUpdates(self._top_level["main"].widget)
|
||||||
|
# if auto_updates.create_default_dock:
|
||||||
|
# auto_updates.start_default_dock()
|
||||||
|
# self._start_update_script()
|
||||||
|
# self._auto_updates = auto_updates
|
||||||
|
self._do_show_all()
|
||||||
|
self._gui_started_event.set()
|
||||||
|
|
||||||
|
def _start_server(self, wait: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Start the GUI server, and execute callback when it is launched
|
||||||
|
"""
|
||||||
|
if self._process is None or self._process.poll() is not None:
|
||||||
|
logger.success("GUI starting...")
|
||||||
|
self._startup_timeout = 5
|
||||||
|
self._gui_started_event.clear()
|
||||||
|
self._process, self._process_output_processing_thread = _start_plot_process(
|
||||||
|
self._gui_id,
|
||||||
|
self.__class__,
|
||||||
|
gui_class_id=self._default_dock_name,
|
||||||
|
config=self._client._service_config.config, # pylint: disable=protected-access
|
||||||
|
logger=logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
def gui_started_callback(callback):
|
||||||
|
try:
|
||||||
|
if callable(callback):
|
||||||
|
callback()
|
||||||
|
finally:
|
||||||
|
threading.current_thread().cancel()
|
||||||
|
|
||||||
|
self._gui_started_timer = RepeatTimer(
|
||||||
|
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
||||||
|
)
|
||||||
|
self._gui_started_timer.start()
|
||||||
|
|
||||||
|
if wait:
|
||||||
|
self._gui_started_event.wait()
|
||||||
|
|
||||||
|
def _dump(self):
|
||||||
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||||
|
return rpc_client._run_rpc("_dump")
|
||||||
|
|
||||||
|
def _start(self, wait: bool = False) -> None:
|
||||||
|
self._killed = False
|
||||||
|
self._client.connector.register(
|
||||||
|
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||||
|
)
|
||||||
|
return self._start_server(wait=wait)
|
||||||
|
|
||||||
|
def _handle_registry_update(self, msg: StreamMessage) -> None:
|
||||||
|
self._registry_state = msg["data"].state
|
||||||
|
self._update_dynamic_namespace()
|
||||||
|
|
||||||
|
def _do_show_all(self):
|
||||||
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||||
|
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
||||||
|
for window in self._top_level.values():
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
def _show_all(self):
|
||||||
|
with wait_for_server(self):
|
||||||
|
return self._do_show_all()
|
||||||
|
|
||||||
|
def _hide_all(self):
|
||||||
|
with wait_for_server(self):
|
||||||
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||||
|
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
||||||
|
# because of the registry callbacks, we may have
|
||||||
|
# dock areas that are already killed, but not yet
|
||||||
|
# removed from the registry state
|
||||||
|
if not self._killed:
|
||||||
|
for window in self._top_level.values():
|
||||||
|
window.hide()
|
||||||
|
|
||||||
|
def _update_dynamic_namespace(self):
|
||||||
|
"""Update the dynamic name space"""
|
||||||
|
self._clear_exposed_dock_areas()
|
||||||
|
self._cleanup_ipython_registry()
|
||||||
|
self._add_registry_to_namespace()
|
||||||
|
|
||||||
|
def _clear_exposed_dock_areas(self):
|
||||||
|
"""Clear the exposed dock areas"""
|
||||||
|
self._top_level.clear()
|
||||||
|
for widget_id in self._exposed_dock_areas:
|
||||||
|
delattr(self, widget_id)
|
||||||
|
self._exposed_dock_areas.clear()
|
||||||
|
|
||||||
|
def _cleanup_ipython_registry(self):
|
||||||
|
"""Cleanup the ipython registry"""
|
||||||
|
remove_ids = []
|
||||||
|
for widget_id in self._ipython_registry:
|
||||||
|
if widget_id not in self._registry_state:
|
||||||
|
remove_ids.append(widget_id)
|
||||||
|
for widget_id in remove_ids:
|
||||||
|
self._ipython_registry.pop(widget_id)
|
||||||
|
|
||||||
|
def _set_dynamic_attributes(self, obj: object, name: str, value: Any) -> None:
|
||||||
|
"""Add an object to the namespace"""
|
||||||
|
setattr(obj, name, value)
|
||||||
|
|
||||||
|
def _add_registry_to_namespace(self) -> None:
|
||||||
|
"""Add registry to namespace"""
|
||||||
|
# Add dock areas
|
||||||
|
dock_area_states = [
|
||||||
|
state
|
||||||
|
for state in self._registry_state.values()
|
||||||
|
if state["widget_class"] == "BECDockArea"
|
||||||
|
]
|
||||||
|
for state in dock_area_states:
|
||||||
|
# obj is an RPC reference to the RPCBase object
|
||||||
|
dock_area_obj = self._add_widget(state, self)
|
||||||
|
self._set_dynamic_attributes(self, dock_area_obj.widget_name, dock_area_obj)
|
||||||
|
# Add dock_area to the top level
|
||||||
|
self._top_level[dock_area_obj.widget_name] = dock_area_obj
|
||||||
|
self._exposed_dock_areas.append(dock_area_obj.widget_name)
|
||||||
|
|
||||||
|
# Add docks
|
||||||
|
dock_states = [
|
||||||
|
state
|
||||||
|
for state in self._registry_state.values()
|
||||||
|
if state["config"].get("parent_id", "") == dock_area_obj._gui_id
|
||||||
|
]
|
||||||
|
for state in dock_states:
|
||||||
|
dock_obj = self._add_widget(state, dock_area_obj)
|
||||||
|
self._set_dynamic_attributes(dock_area_obj, dock_obj.widget_name, dock_obj)
|
||||||
|
|
||||||
|
# Add widgets
|
||||||
|
widget_states = [
|
||||||
|
state
|
||||||
|
for state in self._registry_state.values()
|
||||||
|
if state["config"].get("parent_id", "") == dock_obj._gui_id
|
||||||
|
]
|
||||||
|
for state in widget_states:
|
||||||
|
widget = self._add_widget(state, dock_obj)
|
||||||
|
self._set_dynamic_attributes(dock_obj, widget.widget_name, widget)
|
||||||
|
self._set_dynamic_attributes(dock_area_obj.elements, widget.widget_name, widget)
|
||||||
|
|
||||||
|
def _add_widget(self, state: dict, parent: object) -> RPCReference:
|
||||||
|
"""Add a widget to the namespace
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state (dict): The state of the widget from the _registry_state.
|
||||||
|
parent (object): The parent object.
|
||||||
|
"""
|
||||||
|
name = state["name"]
|
||||||
|
gui_id = state["gui_id"]
|
||||||
|
widget_class = getattr(client, state["widget_class"])
|
||||||
|
obj = self._ipython_registry.get(gui_id)
|
||||||
|
if obj is None:
|
||||||
|
widget = widget_class(gui_id=gui_id, name=name, parent=parent)
|
||||||
|
self._ipython_registry[gui_id] = widget
|
||||||
|
else:
|
||||||
|
widget = obj
|
||||||
|
if widget_class == client.BECDockArea:
|
||||||
|
# Add elements to dynamic namespace
|
||||||
|
self._set_dynamic_attributes(widget, "elements", WidgetNameSpace())
|
||||||
|
obj = RPCReference(registry=self._ipython_registry, gui_id=gui_id)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
################################
|
||||||
|
#### Auto updates ####
|
||||||
|
#### potentially deprecated ####
|
||||||
|
################################
|
||||||
|
|
||||||
# FIXME AUTO UPDATES
|
# FIXME AUTO UPDATES
|
||||||
# @property
|
# @property
|
||||||
# def auto_updates(self):
|
# def auto_updates(self):
|
||||||
@ -235,19 +531,19 @@ class BECGuiClient(RPCBase):
|
|||||||
# with wait_for_server(self):
|
# with wait_for_server(self):
|
||||||
# return self._auto_updates
|
# return self._auto_updates
|
||||||
|
|
||||||
def _get_update_script(self) -> AutoUpdates | None:
|
# def _get_update_script(self) -> AutoUpdates | None:
|
||||||
eps = imd.entry_points(group="bec.widgets.auto_updates")
|
# eps = imd.entry_points(group="bec.widgets.auto_updates")
|
||||||
for ep in eps:
|
# for ep in eps:
|
||||||
if ep.name == "plugin_widgets_update":
|
# if ep.name == "plugin_widgets_update":
|
||||||
try:
|
# try:
|
||||||
spec = importlib.util.find_spec(ep.module)
|
# spec = importlib.util.find_spec(ep.module)
|
||||||
# if the module is not found, we skip it
|
# # if the module is not found, we skip it
|
||||||
if spec is None:
|
# if spec is None:
|
||||||
continue
|
# continue
|
||||||
return ep.load()(gui=self._top_level["main"])
|
# return ep.load()(gui=self._top_level["main"])
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.error(f"Error loading auto update script from plugin: {str(e)}")
|
# logger.error(f"Error loading auto update script from plugin: {str(e)}")
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
# FIXME AUTO UPDATES
|
# FIXME AUTO UPDATES
|
||||||
# @property
|
# @property
|
||||||
@ -292,176 +588,14 @@ class BECGuiClient(RPCBase):
|
|||||||
# if self._auto_updates_enabled:
|
# if self._auto_updates_enabled:
|
||||||
# return self.auto_updates.do_update(msg)
|
# return self.auto_updates.do_update(msg)
|
||||||
|
|
||||||
def _gui_post_startup(self):
|
|
||||||
# if self._auto_updates_enabled:
|
|
||||||
# if self._auto_updates is None:
|
|
||||||
# auto_updates = self._get_update_script()
|
|
||||||
# if auto_updates is None:
|
|
||||||
# AutoUpdates.create_default_dock = True
|
|
||||||
# AutoUpdates.enabled = True
|
|
||||||
# auto_updates = AutoUpdates(self._top_level["main"].widget)
|
|
||||||
# if auto_updates.create_default_dock:
|
|
||||||
# auto_updates.start_default_dock()
|
|
||||||
# self._start_update_script()
|
|
||||||
# self._auto_updates = auto_updates
|
|
||||||
self._top_level[self._default_dock_name] = BECDockArea(
|
|
||||||
gui_id=f"{self._default_dock_name}", name=self._default_dock_name, parent=self
|
|
||||||
)
|
|
||||||
self._do_show_all()
|
|
||||||
self._gui_started_event.set()
|
|
||||||
|
|
||||||
def _start_server(self, wait: bool = False) -> None:
|
|
||||||
"""
|
|
||||||
Start the GUI server, and execute callback when it is launched
|
|
||||||
"""
|
|
||||||
if self._process is None or self._process.poll() is not None:
|
|
||||||
logger.success("GUI starting...")
|
|
||||||
self._startup_timeout = 5
|
|
||||||
self._gui_started_event.clear()
|
|
||||||
self._process, self._process_output_processing_thread = _start_plot_process(
|
|
||||||
self._gui_id,
|
|
||||||
self.__class__,
|
|
||||||
gui_class_id=self._default_dock_name,
|
|
||||||
config=self._client._service_config.config, # pylint: disable=protected-access
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
def gui_started_callback(callback):
|
|
||||||
try:
|
|
||||||
if callable(callback):
|
|
||||||
callback()
|
|
||||||
finally:
|
|
||||||
threading.current_thread().cancel()
|
|
||||||
|
|
||||||
self._gui_started_timer = RepeatTimer(
|
|
||||||
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
|
||||||
)
|
|
||||||
self._gui_started_timer.start()
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
self._gui_started_event.wait()
|
|
||||||
|
|
||||||
def _dump(self):
|
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
|
||||||
return rpc_client._run_rpc("_dump")
|
|
||||||
|
|
||||||
def start(self, wait: bool = True) -> None:
|
|
||||||
"""Start the server and show the GUI window."""
|
|
||||||
return self._start_server(wait=wait)
|
|
||||||
|
|
||||||
def _do_show_all(self):
|
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
|
||||||
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
|
||||||
for window in self._top_level.values():
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
def _show_all(self):
|
|
||||||
with wait_for_server(self):
|
|
||||||
return self._do_show_all()
|
|
||||||
|
|
||||||
def _hide_all(self):
|
|
||||||
with wait_for_server(self):
|
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
|
||||||
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
|
||||||
# because of the registry callbacks, we may have
|
|
||||||
# dock areas that are already killed, but not yet
|
|
||||||
# removed from the registry state
|
|
||||||
if not self._killed:
|
|
||||||
for window in self._top_level.values():
|
|
||||||
window.hide()
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
"""Show the GUI window."""
|
|
||||||
if self._process is not None:
|
|
||||||
return self._show_all()
|
|
||||||
# backward compatibility: show() was also starting server
|
|
||||||
return self._start_server(wait=True)
|
|
||||||
|
|
||||||
def hide(self):
|
|
||||||
"""Hide the GUI window."""
|
|
||||||
return self._hide_all()
|
|
||||||
|
|
||||||
def new(
|
|
||||||
self,
|
|
||||||
name: str | None = None,
|
|
||||||
wait: bool = True,
|
|
||||||
geometry: tuple[int, int, int, int] | None = None,
|
|
||||||
) -> BECDockArea:
|
|
||||||
"""Create a new top-level dock area.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name(str, optional): The name of the dock area. Defaults to None.
|
|
||||||
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
|
|
||||||
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
|
|
||||||
Returns:
|
|
||||||
BECDockArea: The new dock area.
|
|
||||||
"""
|
|
||||||
if len(self.window_list) == 0:
|
|
||||||
self.show()
|
|
||||||
if wait:
|
|
||||||
with wait_for_server(self):
|
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
|
||||||
widget = rpc_client._run_rpc(
|
|
||||||
"new_dock_area", name, geometry
|
|
||||||
) # pylint: disable=protected-access
|
|
||||||
self._top_level[widget.widget_name] = widget
|
|
||||||
return widget
|
|
||||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
|
||||||
widget = rpc_client._run_rpc(
|
|
||||||
"new_dock_area", name, geometry
|
|
||||||
) # pylint: disable=protected-access
|
|
||||||
self._top_level[widget.widget_name] = widget
|
|
||||||
return widget
|
|
||||||
|
|
||||||
def delete(self, name: str) -> None:
|
|
||||||
"""Delete a dock area.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name(str): The name of the dock area.
|
|
||||||
"""
|
|
||||||
widget = self.windows.get(name)
|
|
||||||
if widget is None:
|
|
||||||
raise ValueError(f"Dock area {name} not found.")
|
|
||||||
widget._run_rpc("close") # pylint: disable=protected-access
|
|
||||||
|
|
||||||
def delete_all(self) -> None:
|
|
||||||
"""Delete all dock areas."""
|
|
||||||
for widget_name in self.windows.keys():
|
|
||||||
self.delete(widget_name)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Deprecated. Use kill_server() instead."""
|
|
||||||
# FIXME, deprecated in favor of kill, will be removed in the future
|
|
||||||
self.kill_server()
|
|
||||||
|
|
||||||
def kill_server(self) -> None:
|
|
||||||
"""Kill the GUI server."""
|
|
||||||
self._top_level.clear()
|
|
||||||
self._killed = True
|
|
||||||
|
|
||||||
if self._gui_started_timer is not None:
|
|
||||||
self._gui_started_timer.cancel()
|
|
||||||
self._gui_started_timer.join()
|
|
||||||
|
|
||||||
if self._process is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._process:
|
|
||||||
logger.success("Stopping GUI...")
|
|
||||||
self._process.terminate()
|
|
||||||
if self._process_output_processing_thread:
|
|
||||||
self._process_output_processing_thread.join()
|
|
||||||
self._process.wait()
|
|
||||||
self._process = None
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
from bec_lib.client import BECClient
|
from bec_lib.client import BECClient
|
||||||
from bec_lib.service_config import ServiceConfig
|
from bec_lib.service_config import ServiceConfig
|
||||||
|
|
||||||
config = ServiceConfig()
|
config = ServiceConfig()
|
||||||
client = BECClient(config)
|
bec_client = BECClient(config)
|
||||||
client.start()
|
bec_client.start()
|
||||||
|
|
||||||
# Test the client_utils.py module
|
# Test the client_utils.py module
|
||||||
gui = BECGuiClient()
|
gui = BECGuiClient()
|
||||||
|
@ -60,6 +60,50 @@ class RPCResponseTimeoutError(Exception):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedWidgetError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_deleted_widget(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
if self._gui_id not in self._registry:
|
||||||
|
raise DeletedWidgetError(f"Widget with gui_id {self._gui_id} has been deleted")
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class RPCReference:
|
||||||
|
def __init__(self, registry: dict, gui_id: str) -> None:
|
||||||
|
self._registry = registry
|
||||||
|
self._gui_id = gui_id
|
||||||
|
|
||||||
|
@check_for_deleted_widget
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in ["_registry", "_gui_id"]:
|
||||||
|
return super().__getattribute__(name)
|
||||||
|
return self._registry[self._gui_id].__getattribute__(name)
|
||||||
|
|
||||||
|
@check_for_deleted_widget
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._registry[self._gui_id].__getitem__(key)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self._gui_id not in self._registry:
|
||||||
|
return f"<Deleted widget with gui_id {self._gui_id}>"
|
||||||
|
return self._registry[self._gui_id].__repr__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self._gui_id not in self._registry:
|
||||||
|
return f"<Deleted widget with gui_id {self._gui_id}>"
|
||||||
|
return self._registry[self._gui_id].__str__()
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
if self._gui_id not in self._registry:
|
||||||
|
return []
|
||||||
|
return self._registry[self._gui_id].__dir__()
|
||||||
|
|
||||||
|
|
||||||
class RPCBase:
|
class RPCBase:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -182,7 +226,11 @@ class RPCBase:
|
|||||||
|
|
||||||
cls = getattr(client, cls)
|
cls = getattr(client, cls)
|
||||||
# print(msg_result)
|
# print(msg_result)
|
||||||
return cls(parent=self, **msg_result)
|
ret = cls(parent=self, **msg_result)
|
||||||
|
self._root._ipython_registry[ret._gui_id] = ret
|
||||||
|
obj = RPCReference(self._root._ipython_registry, ret._gui_id)
|
||||||
|
return obj
|
||||||
|
# return ret
|
||||||
return msg_result
|
return msg_result
|
||||||
|
|
||||||
def _gui_is_alive(self):
|
def _gui_is_alive(self):
|
||||||
|
@ -17,6 +17,20 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_update(func):
|
||||||
|
"""
|
||||||
|
Decorator to broadcast updates to the RPCRegister whenever a new RPC object is added or removed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
result = func(self, *args, **kwargs)
|
||||||
|
self.broadcast()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class RPCRegister:
|
class RPCRegister:
|
||||||
"""
|
"""
|
||||||
A singleton class that keeps track of all the RPC objects registered in the system for CLI usage.
|
A singleton class that keeps track of all the RPC objects registered in the system for CLI usage.
|
||||||
@ -37,7 +51,9 @@ class RPCRegister:
|
|||||||
return
|
return
|
||||||
self._rpc_register = WeakValueDictionary()
|
self._rpc_register = WeakValueDictionary()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
self.callbacks = []
|
||||||
|
|
||||||
|
@broadcast_update
|
||||||
def add_rpc(self, rpc: QObject):
|
def add_rpc(self, rpc: QObject):
|
||||||
"""
|
"""
|
||||||
Add an RPC object to the register.
|
Add an RPC object to the register.
|
||||||
@ -49,6 +65,7 @@ class RPCRegister:
|
|||||||
raise ValueError("RPC object must have a 'gui_id' attribute.")
|
raise ValueError("RPC object must have a 'gui_id' attribute.")
|
||||||
self._rpc_register[rpc.gui_id] = rpc
|
self._rpc_register[rpc.gui_id] = rpc
|
||||||
|
|
||||||
|
@broadcast_update
|
||||||
def remove_rpc(self, rpc: str):
|
def remove_rpc(self, rpc: str):
|
||||||
"""
|
"""
|
||||||
Remove an RPC object from the register.
|
Remove an RPC object from the register.
|
||||||
@ -97,6 +114,25 @@ class RPCRegister:
|
|||||||
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
|
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
|
||||||
return [widget._name for widget in widgets]
|
return [widget._name for widget in widgets]
|
||||||
|
|
||||||
|
def broadcast(self):
|
||||||
|
"""
|
||||||
|
Broadcast the update to all the callbacks.
|
||||||
|
"""
|
||||||
|
# print("Broadcasting")
|
||||||
|
connections = self.list_all_connections()
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback(connections)
|
||||||
|
|
||||||
|
def add_callback(self, callback: Callable[[dict], None]):
|
||||||
|
"""
|
||||||
|
Add a callback that will be called whenever the registry is updated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback(Callable[[dict], None]): The callback to be added. It should accept a dictionary of all the
|
||||||
|
registered RPC objects as an argument.
|
||||||
|
"""
|
||||||
|
self.callbacks.append(callback)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_singleton(cls):
|
def reset_singleton(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -69,6 +69,7 @@ class BECWidgetsCLIServer:
|
|||||||
self.gui_id = gui_id
|
self.gui_id = gui_id
|
||||||
# register broadcast callback
|
# register broadcast callback
|
||||||
self.rpc_register = RPCRegister()
|
self.rpc_register = RPCRegister()
|
||||||
|
self.rpc_register.add_callback(self.broadcast_registry_update)
|
||||||
self.gui = gui_class(parent=None, name=gui_class_id, gui_id=gui_class_id)
|
self.gui = gui_class(parent=None, name=gui_class_id, gui_id=gui_class_id)
|
||||||
# self.rpc_register.add_rpc(self.gui)
|
# self.rpc_register.add_rpc(self.gui)
|
||||||
|
|
||||||
@ -139,13 +140,15 @@ class BECWidgetsCLIServer:
|
|||||||
|
|
||||||
def serialize_object(self, obj):
|
def serialize_object(self, obj):
|
||||||
if isinstance(obj, BECConnector):
|
if isinstance(obj, BECConnector):
|
||||||
|
config = {} # obj.config.model_dump()
|
||||||
|
config["parent_id"] = obj.parent_id
|
||||||
return {
|
return {
|
||||||
"gui_id": obj.gui_id,
|
"gui_id": obj.gui_id,
|
||||||
"name": (
|
"name": (
|
||||||
obj._name if hasattr(obj, "_name") else obj.__class__.__name__
|
obj._name if hasattr(obj, "_name") else obj.__class__.__name__
|
||||||
), # pylint: disable=protected-access
|
), # pylint: disable=protected-access
|
||||||
"widget_class": obj.__class__.__name__,
|
"widget_class": obj.__class__.__name__,
|
||||||
"config": obj.config.model_dump(),
|
"config": config,
|
||||||
"__rpc__": True,
|
"__rpc__": True,
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
@ -161,6 +164,22 @@ class BECWidgetsCLIServer:
|
|||||||
except RedisError as exc:
|
except RedisError as exc:
|
||||||
logger.error(f"Error while emitting heartbeat: {exc}")
|
logger.error(f"Error while emitting heartbeat: {exc}")
|
||||||
|
|
||||||
|
def broadcast_registry_update(self, connections: dict):
|
||||||
|
"""
|
||||||
|
Broadcast the updated registry to all clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We only need to broadcast the dock areas
|
||||||
|
data = {key: self.serialize_object(val) for key, val in connections.items()}
|
||||||
|
# logger.info(f"All registered connections: {list(connections.keys())}")
|
||||||
|
for k, v in data.items():
|
||||||
|
logger.info(f"key: {k}, value: {v}")
|
||||||
|
self.client.connector.xadd(
|
||||||
|
MessageEndpoints.gui_registry_state(self.gui_id),
|
||||||
|
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
|
||||||
|
max_size=1, # only single message in stream
|
||||||
|
)
|
||||||
|
|
||||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||||
logger.info(f"Shutting down server with gui_id: {self.gui_id}")
|
logger.info(f"Shutting down server with gui_id: {self.gui_id}")
|
||||||
self.status = messages.BECStatus.IDLE
|
self.status = messages.BECStatus.IDLE
|
||||||
|
@ -82,6 +82,7 @@ class BECConnector:
|
|||||||
config: ConnectionConfig | None = None,
|
config: ConnectionConfig | None = None,
|
||||||
gui_id: str | None = None,
|
gui_id: str | None = None,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
|
parent_id: str | None = None,
|
||||||
):
|
):
|
||||||
# BEC related connections
|
# BEC related connections
|
||||||
self.bec_dispatcher = BECDispatcher(client=client)
|
self.bec_dispatcher = BECDispatcher(client=client)
|
||||||
@ -110,14 +111,12 @@ class BECConnector:
|
|||||||
)
|
)
|
||||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||||
|
|
||||||
# I feel that we should not allow BECConnector to be created with a custom gui_id
|
self.parent_id = parent_id
|
||||||
# because this would break with the logic in the RPCRegister of retrieving widgets by type
|
# If the gui_id is passed, it should be respected. However, this should be revisted since
|
||||||
# iterating over all widgets and checkinf if the register widget starts with the string that is passsed.
|
# the gui_id has to be unique, and may no longer be.
|
||||||
# If the gui_id is randomly generated, this would break since that widget would have a
|
|
||||||
# gui_id that is generated in a different way.
|
|
||||||
if gui_id:
|
if gui_id:
|
||||||
self.config.gui_id = gui_id
|
self.config.gui_id = gui_id
|
||||||
self.gui_id: str = gui_id
|
self.gui_id: str = gui_id # Keep namespace in sync
|
||||||
else:
|
else:
|
||||||
self.gui_id: str = self.config.gui_id # type: ignore
|
self.gui_id: str = self.config.gui_id # type: ignore
|
||||||
if name is None:
|
if name is None:
|
||||||
@ -319,6 +318,7 @@ class BECConnector:
|
|||||||
self.deleteLater()
|
self.deleteLater()
|
||||||
else:
|
else:
|
||||||
self.rpc_register.remove_rpc(self)
|
self.rpc_register.remove_rpc(self)
|
||||||
|
self.rpc_register.broadcast()
|
||||||
|
|
||||||
def get_config(self, dict_output: bool = True) -> dict | BaseModel:
|
def get_config(self, dict_output: bool = True) -> dict | BaseModel:
|
||||||
"""
|
"""
|
||||||
|
@ -34,6 +34,7 @@ class BECWidget(BECConnector):
|
|||||||
theme_update: bool = False,
|
theme_update: bool = False,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
parent_dock: BECDock | None = None,
|
parent_dock: BECDock | None = None,
|
||||||
|
parent_id: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -56,7 +57,9 @@ class BECWidget(BECConnector):
|
|||||||
if not isinstance(self, QWidget):
|
if not isinstance(self, QWidget):
|
||||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||||
|
|
||||||
super().__init__(client=client, config=config, gui_id=gui_id, name=name)
|
super().__init__(
|
||||||
|
client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
|
||||||
|
)
|
||||||
self._parent_dock = parent_dock
|
self._parent_dock = parent_dock
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if not hasattr(app, "theme"):
|
if not hasattr(app, "theme"):
|
||||||
|
@ -131,6 +131,7 @@ class BECDock(BECWidget, Dock):
|
|||||||
self,
|
self,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
parent_dock_area: BECDockArea | None = None,
|
parent_dock_area: BECDockArea | None = None,
|
||||||
|
parent_id: str | None = None,
|
||||||
config: DockConfig | None = None,
|
config: DockConfig | None = None,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
client=None,
|
client=None,
|
||||||
@ -149,7 +150,7 @@ class BECDock(BECWidget, Dock):
|
|||||||
config = DockConfig(**config)
|
config = DockConfig(**config)
|
||||||
self.config = config
|
self.config = config
|
||||||
super().__init__(
|
super().__init__(
|
||||||
client=client, config=config, gui_id=gui_id, name=name
|
client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
|
||||||
) # Name was checked and created in BEC Widget
|
) # Name was checked and created in BEC Widget
|
||||||
label = CustomDockLabel(text=name, closable=closable)
|
label = CustomDockLabel(text=name, closable=closable)
|
||||||
Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
|
Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
|
||||||
@ -324,18 +325,24 @@ class BECDock(BECWidget, Dock):
|
|||||||
if isinstance(widget, str):
|
if isinstance(widget, str):
|
||||||
widget = cast(
|
widget = cast(
|
||||||
BECWidget,
|
BECWidget,
|
||||||
widget_handler.create_widget(widget_type=widget, name=name, parent_dock=self),
|
widget_handler.create_widget(
|
||||||
|
widget_type=widget, name=name, parent_dock=self, parent_id=self.gui_id
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
widget._name = name # pylint: disable=protected-access
|
widget._name = name # pylint: disable=protected-access
|
||||||
|
|
||||||
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||||
|
|
||||||
if hasattr(widget, "config"):
|
if hasattr(widget, "config"):
|
||||||
self.config.widgets[widget.gui_id] = widget.config
|
widget.config.gui_id = widget.gui_id
|
||||||
|
self.config.widgets[widget._name] = widget.config # pylint: disable=protected-access
|
||||||
|
self._broadcast_update()
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
def _broadcast_update(self):
|
||||||
|
rpc_register = RPCRegister()
|
||||||
|
rpc_register.broadcast()
|
||||||
|
|
||||||
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
||||||
"""
|
"""
|
||||||
Move a widget to a new position in the layout.
|
Move a widget to a new position in the layout.
|
||||||
|
@ -357,7 +357,7 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
else: # Name is not provided
|
else: # Name is not provided
|
||||||
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
||||||
|
|
||||||
dock = BECDock(name=name, parent_dock_area=self, closable=closable)
|
dock = BECDock(name=name, parent_dock_area=self, parent_id=self.gui_id, closable=closable)
|
||||||
dock.config.position = position
|
dock.config.position = position
|
||||||
self.config.docks[dock.name()] = dock.config
|
self.config.docks[dock.name()] = dock.config
|
||||||
# The dock.name is equal to the name passed to BECDock
|
# The dock.name is equal to the name passed to BECDock
|
||||||
@ -381,8 +381,14 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
self.update()
|
self.update()
|
||||||
if floating:
|
if floating:
|
||||||
dock.detach()
|
dock.detach()
|
||||||
|
# Run broadcast update
|
||||||
|
self._broadcast_update()
|
||||||
return dock
|
return dock
|
||||||
|
|
||||||
|
def _broadcast_update(self):
|
||||||
|
rpc_register = RPCRegister()
|
||||||
|
rpc_register.broadcast()
|
||||||
|
|
||||||
def detach_dock(self, dock_name: str) -> BECDock:
|
def detach_dock(self, dock_name: str) -> BECDock:
|
||||||
"""
|
"""
|
||||||
Undock a dock from the dock area.
|
Undock a dock from the dock area.
|
||||||
|
@ -76,6 +76,7 @@ class BECMotorMap(BECPlotBase):
|
|||||||
config: Optional[MotorMapConfig] = None,
|
config: Optional[MotorMapConfig] = None,
|
||||||
client=None,
|
client=None,
|
||||||
gui_id: Optional[str] = None,
|
gui_id: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
if config is None:
|
if config is None:
|
||||||
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
||||||
|
@ -274,4 +274,4 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
|||||||
"""Remove the curve from the plot."""
|
"""Remove the curve from the plot."""
|
||||||
# self.parent_item.removeItem(self)
|
# self.parent_item.removeItem(self)
|
||||||
self.parent_item.remove_curve(self.name())
|
self.parent_item.remove_curve(self.name())
|
||||||
self.rpc_register.remove_rpc(self)
|
super().remove()
|
||||||
|
@ -325,4 +325,4 @@ class Curve(BECConnector, pg.PlotDataItem):
|
|||||||
"""Remove the curve from the plot."""
|
"""Remove the curve from the plot."""
|
||||||
# self.parent_item.removeItem(self)
|
# self.parent_item.removeItem(self)
|
||||||
self.parent_item.remove_curve(self.name())
|
self.parent_item.remove_curve(self.name())
|
||||||
self.rpc_register.remove_rpc(self)
|
super().remove()
|
||||||
|
@ -1,81 +1,59 @@
|
|||||||
|
"""This module contains fixtures that are used in the end-2-end tests."""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import time
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
|
||||||
|
|
||||||
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
from bec_widgets.utils import BECDispatcher
|
|
||||||
from bec_widgets.widgets.containers.figure import BECFigure
|
# pylint: disable=unused-argument
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
|
||||||
|
|
||||||
# make threads check in autouse, **will be executed at the end**; better than
|
|
||||||
# having it in fixtures for each test, since it prevents from needing to
|
|
||||||
# 'manually' shutdown bec_client_lib (for example) to make it happy, whereas
|
|
||||||
# whereas in fact bec_client_lib makes its on cleanup
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def threads_check_fixture(threads_check):
|
def threads_check_fixture(threads_check):
|
||||||
|
"""
|
||||||
|
Fixture to check if threads are still alive at the end of the test.
|
||||||
|
|
||||||
|
This should always run to avoid leaked threads within our application.
|
||||||
|
The fixture is set to autouse, meaning it will run for every test.
|
||||||
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def gui_id():
|
def gui_id():
|
||||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
|
||||||
|
return f"figure_{random.randint(0,100)}"
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def plot_server(gui_id, klass, client_lib):
|
|
||||||
dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client
|
|
||||||
process, _ = _start_plot_process(
|
|
||||||
gui_id, klass, gui_class_id="bec", config=client_lib._client._service_config.config_path
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None:
|
|
||||||
time.sleep(0.3)
|
|
||||||
yield gui_id
|
|
||||||
finally:
|
|
||||||
process.terminate()
|
|
||||||
process.wait()
|
|
||||||
dispatcher.disconnect_all()
|
|
||||||
dispatcher.reset_singleton()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def connected_client_figure(gui_id, bec_client_lib):
|
|
||||||
with plot_server(gui_id, BECFigure, bec_client_lib) as server:
|
|
||||||
|
|
||||||
yield server
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def connected_client_gui_obj(gui_id, bec_client_lib):
|
def connected_client_gui_obj(gui_id, bec_client_lib):
|
||||||
|
"""
|
||||||
|
Fixture to create a new BECGuiClient object and start a server in the background.
|
||||||
|
|
||||||
|
This fixture should be used if a new gui instance is needed for each test.
|
||||||
|
"""
|
||||||
gui = BECGuiClient(gui_id=gui_id)
|
gui = BECGuiClient(gui_id=gui_id)
|
||||||
try:
|
try:
|
||||||
gui.start(wait=True)
|
gui.start(wait=True)
|
||||||
# gui._start_server(wait=True)
|
|
||||||
yield gui
|
yield gui
|
||||||
finally:
|
finally:
|
||||||
gui.kill_server()
|
gui.kill_server()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="session")
|
||||||
def connected_client_dock(gui_id, bec_client_lib):
|
def connected_gui_with_scope_session(gui_id, bec_client_lib):
|
||||||
|
"""
|
||||||
|
Fixture to create a new BECGuiClient object and start a server in the background.
|
||||||
|
|
||||||
|
This fixture is scoped to the session, meaning it remains alive for all tests in the session.
|
||||||
|
We can use this fixture to create a gui object that is used across multiple tests, and
|
||||||
|
simulate a real-world scenario where the gui is not restarted for each test.
|
||||||
|
"""
|
||||||
gui = BECGuiClient(gui_id=gui_id)
|
gui = BECGuiClient(gui_id=gui_id)
|
||||||
gui._auto_updates_enabled = False
|
|
||||||
try:
|
try:
|
||||||
gui.start(wait=True)
|
gui.start(wait=True)
|
||||||
gui.window_list[0]
|
yield gui
|
||||||
yield gui.window_list[0]
|
|
||||||
finally:
|
|
||||||
gui.kill_server()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def connected_client_dock_w_auto_updates(gui_id, bec_client_lib):
|
|
||||||
gui = BECGuiClient(gui_id=gui_id)
|
|
||||||
try:
|
|
||||||
gui._start_server(wait=True)
|
|
||||||
yield gui, gui.window_list[0]
|
|
||||||
finally:
|
finally:
|
||||||
gui.kill_server()
|
gui.kill_server()
|
||||||
|
@ -1,378 +1,284 @@
|
|||||||
# import time
|
import time
|
||||||
|
|
||||||
# import numpy as np
|
import numpy as np
|
||||||
# import pytest
|
import pytest
|
||||||
# from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
|
||||||
# from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||||
# from bec_widgets.tests.utils import check_remote_data_size
|
from bec_widgets.cli.rpc.rpc_base import RPCReference
|
||||||
# from bec_widgets.utils import Colors
|
from bec_widgets.tests.utils import check_remote_data_size
|
||||||
|
from bec_widgets.utils import Colors
|
||||||
# # pylint: disable=unused-argument
|
|
||||||
# # pylint: disable=redefined-outer-name
|
# pylint: disable=unused-argument
|
||||||
# # pylint: disable=too-many-locals
|
# pylint: disable=redefined-outer-name
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
# pylint: disable=protected-access
|
||||||
# def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_dock):
|
|
||||||
# # BEC client shortcuts
|
|
||||||
# dock = connected_client_dock
|
def test_gui_rpc_registry(qtbot, connected_client_gui_obj):
|
||||||
# client = bec_client_lib
|
gui = connected_client_gui_obj
|
||||||
# dev = client.device_manager.devices
|
dock_area = gui.new("cool_dock_area")
|
||||||
# scans = client.scans
|
|
||||||
# queue = client.queue
|
def check_dock_area_registered():
|
||||||
|
return dock_area._gui_id in gui._registry_state
|
||||||
# # Create 3 docks
|
|
||||||
# d0 = dock.add_dock("dock_0")
|
qtbot.waitUntil(check_dock_area_registered, timeout=5000)
|
||||||
# d1 = dock.add_dock("dock_1")
|
assert hasattr(gui, "cool_dock_area")
|
||||||
# d2 = dock.add_dock("dock_2")
|
|
||||||
|
dock = dock_area.new("dock_0")
|
||||||
# dock_config = dock._config_dict
|
|
||||||
# assert len(dock_config["docks"]) == 3
|
def check_dock_registered():
|
||||||
# # Add 3 figures with some widgets
|
dock_dict = (
|
||||||
# fig0 = d0.add_widget("BECFigure")
|
gui._registry_state.get(dock_area._gui_id, {}).get("config", {}).get("docks", {})
|
||||||
# fig1 = d1.add_widget("BECFigure")
|
)
|
||||||
# fig2 = d2.add_widget("BECFigure")
|
return len(dock_dict) == 1
|
||||||
|
|
||||||
# dock_config = dock._config_dict
|
qtbot.waitUntil(check_dock_registered, timeout=5000)
|
||||||
# assert len(dock_config["docks"]) == 3
|
assert hasattr(gui.cool_dock_area, "dock_0")
|
||||||
# assert len(dock_config["docks"]["dock_0"]["widgets"]) == 1
|
|
||||||
# assert len(dock_config["docks"]["dock_1"]["widgets"]) == 1
|
# assert hasattr(dock_area, "dock_0")
|
||||||
# assert len(dock_config["docks"]["dock_2"]["widgets"]) == 1
|
|
||||||
|
|
||||||
# assert fig1.__class__.__name__ == "BECFigure"
|
def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_gui_obj):
|
||||||
# assert fig1.__class__ == BECFigure
|
|
||||||
# assert fig2.__class__.__name__ == "BECFigure"
|
gui = connected_client_gui_obj
|
||||||
# assert fig2.__class__ == BECFigure
|
# BEC client shortcuts
|
||||||
|
dock = gui.bec
|
||||||
# mm = fig0.motor_map("samx", "samy")
|
client = bec_client_lib
|
||||||
# plt = fig1.plot(x_name="samx", y_name="bpm4i")
|
dev = client.device_manager.devices
|
||||||
# im = fig2.image("eiger")
|
scans = client.scans
|
||||||
|
queue = client.queue
|
||||||
# assert mm.__class__.__name__ == "BECMotorMap"
|
|
||||||
# assert mm.__class__ == BECMotorMap
|
# Create 3 docks
|
||||||
# assert plt.__class__.__name__ == "BECWaveform"
|
d0 = dock.new("dock_0")
|
||||||
# assert plt.__class__ == BECWaveform
|
d1 = dock.new("dock_1")
|
||||||
# assert im.__class__.__name__ == "BECImageShow"
|
d2 = dock.new("dock_2")
|
||||||
# assert im.__class__ == BECImageShow
|
|
||||||
|
# Check that callback for dock_registry is done
|
||||||
# assert mm._config_dict["signals"] == {
|
def check_docks_registered():
|
||||||
# "dap": None,
|
dock_register = dock._parent._registry_state.get(dock._gui_id, None)
|
||||||
# "source": "device_readback",
|
if dock_register is not None:
|
||||||
# "x": {
|
n_docks = dock_register.get("config", {}).get("docks", {})
|
||||||
# "name": "samx",
|
return len(n_docks) == 3
|
||||||
# "entry": "samx",
|
return False
|
||||||
# "unit": None,
|
# raise AssertionError("Docks not registered yet")
|
||||||
# "modifier": None,
|
|
||||||
# "limits": [-50.0, 50.0],
|
|
||||||
# },
|
|
||||||
# "y": {
|
|
||||||
# "name": "samy",
|
|
||||||
# "entry": "samy",
|
|
||||||
# "unit": None,
|
|
||||||
# "modifier": None,
|
|
||||||
# "limits": [-50.0, 50.0],
|
|
||||||
# },
|
|
||||||
# "z": None,
|
|
||||||
# }
|
|
||||||
# assert plt._config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
|
|
||||||
# "dap": None,
|
|
||||||
# "source": "scan_segment",
|
|
||||||
# "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
|
||||||
# "y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
|
||||||
# "z": None,
|
|
||||||
# }
|
|
||||||
# assert im._config_dict["images"]["eiger"]["monitor"] == "eiger"
|
|
||||||
|
|
||||||
# # check initial position of motor map
|
|
||||||
# initial_pos_x = dev.samx.read()["samx"]["value"]
|
|
||||||
# initial_pos_y = dev.samy.read()["samy"]["value"]
|
|
||||||
|
|
||||||
# # Try to make a scan
|
|
||||||
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
|
||||||
# status.wait()
|
|
||||||
|
|
||||||
# # plot
|
|
||||||
# item = queue.scan_storage.storage[-1]
|
|
||||||
# plt_last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
|
||||||
# num_elements = 10
|
|
||||||
|
|
||||||
# plot_name = "bpm4i-bpm4i"
|
|
||||||
|
|
||||||
# qtbot.waitUntil(lambda: check_remote_data_size(plt, plot_name, num_elements))
|
|
||||||
|
|
||||||
# plt_data = plt.get_all_data()
|
|
||||||
# assert plt_data["bpm4i-bpm4i"]["x"] == plt_last_scan_data["samx"]["samx"].val
|
|
||||||
# assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
|
|
||||||
|
|
||||||
# # image
|
|
||||||
# last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
|
|
||||||
# "data"
|
|
||||||
# ].data
|
|
||||||
# time.sleep(0.5)
|
|
||||||
# last_image_plot = im.images[0].get_data()
|
|
||||||
# np.testing.assert_equal(last_image_device, last_image_plot)
|
|
||||||
|
|
||||||
# # motor map
|
|
||||||
# final_pos_x = dev.samx.read()["samx"]["value"]
|
|
||||||
# final_pos_y = dev.samy.read()["samy"]["value"]
|
|
||||||
|
|
||||||
# # check final coordinates of motor map
|
|
||||||
# motor_map_data = mm.get_data()
|
|
||||||
|
|
||||||
# np.testing.assert_equal(
|
|
||||||
# [motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
|
|
||||||
# )
|
|
||||||
# np.testing.assert_equal(
|
|
||||||
# [motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
# def test_dock_manipulations_e2e(connected_client_dock):
|
|
||||||
# dock = connected_client_dock
|
|
||||||
|
|
||||||
# d0 = dock.add_dock("dock_0")
|
|
||||||
# d1 = dock.add_dock("dock_1")
|
|
||||||
# d2 = dock.add_dock("dock_2")
|
|
||||||
# dock_config = dock._config_dict
|
|
||||||
# assert len(dock_config["docks"]) == 3
|
|
||||||
|
|
||||||
# d0.detach()
|
|
||||||
# dock.detach_dock("dock_2")
|
|
||||||
# dock_config = dock._config_dict
|
|
||||||
# assert len(dock_config["docks"]) == 3
|
|
||||||
# assert len(dock.temp_areas) == 2
|
|
||||||
|
|
||||||
# d0.attach()
|
|
||||||
# dock_config = dock._config_dict
|
|
||||||
# assert len(dock_config["docks"]) == 3
|
|
||||||
# assert len(dock.temp_areas) == 1
|
|
||||||
|
|
||||||
# d2.remove()
|
|
||||||
# dock_config = dock._config_dict
|
|
||||||
|
|
||||||
# assert ["dock_0", "dock_1"] == list(dock_config["docks"])
|
|
||||||
|
|
||||||
# dock.clear_all()
|
|
||||||
|
|
||||||
# dock_config = dock._config_dict
|
|
||||||
# assert len(dock_config["docks"]) == 0
|
|
||||||
# assert len(dock.temp_areas) == 0
|
|
||||||
|
|
||||||
|
|
||||||
# def test_ring_bar(connected_client_dock):
|
|
||||||
# dock = connected_client_dock
|
|
||||||
|
|
||||||
# d0 = dock.add_dock(name="dock_0")
|
|
||||||
|
|
||||||
# bar = d0.add_widget("RingProgressBar")
|
|
||||||
# assert bar.__class__.__name__ == "RingProgressBar"
|
|
||||||
|
|
||||||
# bar.set_number_of_bars(5)
|
# Waii until docks are registered
|
||||||
# bar.set_colors_from_map("viridis")
|
qtbot.waitUntil(check_docks_registered, timeout=5000)
|
||||||
# bar.set_value([10, 20, 30, 40, 50])
|
qtbot.wait(500)
|
||||||
|
assert len(dock.panels) == 3
|
||||||
|
assert hasattr(gui.bec, "dock_0")
|
||||||
|
|
||||||
# bar_config = bar._config_dict
|
# Add 3 figures with some widgets
|
||||||
|
fig0 = d0.new("BECFigure")
|
||||||
|
fig1 = d1.new("BECFigure")
|
||||||
|
fig2 = d2.new("BECFigure")
|
||||||
|
|
||||||
# expected_colors_light = [
|
def check_fig2_registered():
|
||||||
# list(color) for color in Colors.golden_angle_color("viridis", 5, "RGB", theme="light")
|
# return hasattr(d2, "BECFigure_2")
|
||||||
# ]
|
dock_config = dock._parent._registry_state[dock._gui_id]["config"]["docks"].get(
|
||||||
# expected_colors_dark = [
|
d2.widget_name, {}
|
||||||
# list(color) for color in Colors.golden_angle_color("viridis", 5, "RGB", theme="dark")
|
)
|
||||||
# ]
|
if dock_config:
|
||||||
# bar_colors = [ring._config_dict["color"] for ring in bar.rings]
|
n_widgets = dock_config.get("widgets", {})
|
||||||
# bar_values = [ring._config_dict["value"] for ring in bar.rings]
|
if any(widget_name.startswith("BECFigure") for widget_name in n_widgets.keys()):
|
||||||
# assert bar_config["num_bars"] == 5
|
return True
|
||||||
# assert bar_values == [10, 20, 30, 40, 50]
|
raise AssertionError("Figure not registered yet")
|
||||||
# assert bar_colors == expected_colors_light or bar_colors == expected_colors_dark
|
|
||||||
|
|
||||||
|
qtbot.waitUntil(check_fig2_registered, timeout=5000)
|
||||||
|
|
||||||
# def test_ring_bar_scan_update(bec_client_lib, connected_client_dock):
|
assert len(d0.element_list) == 1
|
||||||
# dock = connected_client_dock
|
assert len(d1.element_list) == 1
|
||||||
|
assert len(d2.element_list) == 1
|
||||||
|
|
||||||
# d0 = dock.add_dock("dock_0")
|
assert fig1.__class__.__name__ == "RPCReference"
|
||||||
|
assert fig1.__class__ == RPCReference
|
||||||
|
assert gui._ipython_registry[fig1._gui_id].__class__ == BECFigure
|
||||||
|
assert fig2.__class__.__name__ == "RPCReference"
|
||||||
|
assert fig2.__class__ == RPCReference
|
||||||
|
assert gui._ipython_registry[fig2._gui_id].__class__ == BECFigure
|
||||||
|
|
||||||
# bar = d0.add_widget("RingProgressBar")
|
mm = fig0.motor_map("samx", "samy")
|
||||||
|
plt = fig1.plot(x_name="samx", y_name="bpm4i")
|
||||||
|
im = fig2.image("eiger")
|
||||||
|
|
||||||
# client = bec_client_lib
|
assert mm.__class__.__name__ == "RPCReference"
|
||||||
# dev = client.device_manager.devices
|
assert mm.__class__ == RPCReference
|
||||||
# dev.samx.tolerance.set(0)
|
assert plt.__class__.__name__ == "RPCReference"
|
||||||
# dev.samy.tolerance.set(0)
|
assert plt.__class__ == RPCReference
|
||||||
# scans = client.scans
|
assert im.__class__.__name__ == "RPCReference"
|
||||||
|
assert im.__class__ == RPCReference
|
||||||
|
|
||||||
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
|
||||||
# status.wait()
|
|
||||||
|
|
||||||
# bar_config = bar._config_dict
|
def test_dock_manipulations_e2e(qtbot, connected_client_gui_obj):
|
||||||
# assert bar_config["num_bars"] == 1
|
gui = connected_client_gui_obj
|
||||||
# assert bar_config["rings"][0]["value"] == 10
|
dock = gui.bec
|
||||||
# assert bar_config["rings"][0]["min_value"] == 0
|
|
||||||
# assert bar_config["rings"][0]["max_value"] == 10
|
|
||||||
|
|
||||||
# status = scans.grid_scan(dev.samx, -5, 5, 4, dev.samy, -10, 10, 4, relative=True, exp_time=0.1)
|
d0 = dock.new("dock_0")
|
||||||
# status.wait()
|
d1 = dock.new("dock_1")
|
||||||
|
d2 = dock.new("dock_2")
|
||||||
|
|
||||||
# bar_config = bar._config_dict
|
assert hasattr(gui.bec, "dock_0")
|
||||||
# assert bar_config["num_bars"] == 1
|
assert hasattr(gui.bec, "dock_1")
|
||||||
# assert bar_config["rings"][0]["value"] == 16
|
assert hasattr(gui.bec, "dock_2")
|
||||||
# assert bar_config["rings"][0]["min_value"] == 0
|
assert len(gui.bec.panels) == 3
|
||||||
# assert bar_config["rings"][0]["max_value"] == 16
|
|
||||||
|
|
||||||
# init_samx = dev.samx.read()["samx"]["value"]
|
d0.detach()
|
||||||
# init_samy = dev.samy.read()["samy"]["value"]
|
dock.detach_dock("dock_2")
|
||||||
# final_samx = init_samx + 5
|
# How can we properly check that the dock is detached?
|
||||||
# final_samy = init_samy + 10
|
assert len(gui.bec.panels) == 3
|
||||||
|
|
||||||
|
d0.attach()
|
||||||
|
assert len(gui.bec.panels) == 3
|
||||||
|
|
||||||
# dev.samx.velocity.put(5)
|
def wait_for_dock_removed():
|
||||||
# dev.samy.velocity.put(5)
|
dock_config = gui._registry_state[gui.bec._gui_id]["config"]["docks"]
|
||||||
|
return len(dock_config.keys()) == 2
|
||||||
# status = scans.umv(dev.samx, 5, dev.samy, 10, relative=True)
|
|
||||||
# status.wait()
|
d2.remove()
|
||||||
|
qtbot.waitUntil(wait_for_dock_removed, timeout=5000)
|
||||||
# bar_config = bar._config_dict
|
assert len(gui.bec.panels) == 2
|
||||||
# assert bar_config["num_bars"] == 2
|
|
||||||
# assert bar_config["rings"][0]["value"] == final_samx
|
def wait_for_docks_removed():
|
||||||
# assert bar_config["rings"][1]["value"] == final_samy
|
dock_config = gui._registry_state[gui.bec._gui_id]["config"]["docks"]
|
||||||
# assert bar_config["rings"][0]["min_value"] == init_samx
|
return len(dock_config.keys()) == 0
|
||||||
# assert bar_config["rings"][0]["max_value"] == final_samx
|
|
||||||
# assert bar_config["rings"][1]["min_value"] == init_samy
|
dock.clear_all()
|
||||||
# assert bar_config["rings"][1]["max_value"] == final_samy
|
qtbot.waitUntil(wait_for_docks_removed, timeout=5000)
|
||||||
|
assert len(gui.bec.panels) == 0
|
||||||
|
|
||||||
# def test_auto_update(bec_client_lib, connected_client_dock_w_auto_updates, qtbot):
|
|
||||||
# client = bec_client_lib
|
def test_ring_bar(connected_client_dock):
|
||||||
# dev = client.device_manager.devices
|
dock = connected_client_dock
|
||||||
# scans = client.scans
|
|
||||||
# queue = client.queue
|
d0 = dock.add_dock(name="dock_0")
|
||||||
# gui, dock = connected_client_dock_w_auto_updates
|
|
||||||
# auto_updates = gui.auto_updates
|
bar = d0.add_widget("RingProgressBar")
|
||||||
|
assert bar.__class__.__name__ == "RingProgressBar"
|
||||||
# def get_default_figure():
|
|
||||||
# return auto_updates.get_default_figure()
|
plt = get_default_figure()
|
||||||
|
|
||||||
# plt = get_default_figure()
|
gui.selected_device = "bpm4i"
|
||||||
|
|
||||||
# gui.selected_device = "bpm4i"
|
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||||
|
status.wait()
|
||||||
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
|
||||||
# status.wait()
|
# get data from curves
|
||||||
|
widgets = plt.widget_list
|
||||||
# # get data from curves
|
qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
||||||
# widgets = plt.widget_list
|
|
||||||
# qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
item = queue.scan_storage.storage[-1]
|
||||||
|
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||||
# item = queue.scan_storage.storage[-1]
|
|
||||||
# last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
num_elements = 10
|
||||||
|
|
||||||
# num_elements = 10
|
plot_name = f"Scan {status.scan.scan_number} - {dock.selected_device}"
|
||||||
|
|
||||||
# plot_name = f"Scan {status.scan.scan_number} - {dock.selected_device}"
|
qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements))
|
||||||
|
plt_data = widgets[0].get_all_data()
|
||||||
# qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements))
|
|
||||||
# plt_data = widgets[0].get_all_data()
|
# check plotted data
|
||||||
|
assert (
|
||||||
# # check plotted data
|
plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"]
|
||||||
# assert (
|
== last_scan_data["samx"]["samx"].val
|
||||||
# plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"]
|
)
|
||||||
# == last_scan_data["samx"]["samx"].val
|
assert (
|
||||||
# )
|
plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["y"]
|
||||||
# assert (
|
== last_scan_data["bpm4i"]["bpm4i"].val
|
||||||
# plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["y"]
|
)
|
||||||
# == last_scan_data["bpm4i"]["bpm4i"].val
|
|
||||||
# )
|
status = scans.grid_scan(
|
||||||
|
dev.samx, -10, 10, 5, dev.samy, -5, 5, 5, exp_time=0.05, relative=False
|
||||||
# status = scans.grid_scan(
|
)
|
||||||
# dev.samx, -10, 10, 5, dev.samy, -5, 5, 5, exp_time=0.05, relative=False
|
status.wait()
|
||||||
# )
|
|
||||||
# status.wait()
|
plt = auto_updates.get_default_figure()
|
||||||
|
widgets = plt.widget_list
|
||||||
# plt = auto_updates.get_default_figure()
|
|
||||||
# widgets = plt.widget_list
|
qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
||||||
|
|
||||||
# qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
item = queue.scan_storage.storage[-1]
|
||||||
|
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||||
# item = queue.scan_storage.storage[-1]
|
|
||||||
# last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
plot_name = f"Scan {status.scan.scan_number} - bpm4i"
|
||||||
|
|
||||||
# plot_name = f"Scan {status.scan.scan_number} - bpm4i"
|
num_elements_bec = 25
|
||||||
|
qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements_bec))
|
||||||
# num_elements_bec = 25
|
plt_data = widgets[0].get_all_data()
|
||||||
# qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements_bec))
|
|
||||||
# plt_data = widgets[0].get_all_data()
|
# check plotted data
|
||||||
|
assert (
|
||||||
# # check plotted data
|
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"]
|
||||||
# assert (
|
== last_scan_data["samx"]["samx"].val
|
||||||
# plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"]
|
)
|
||||||
# == last_scan_data["samx"]["samx"].val
|
assert (
|
||||||
# )
|
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"]
|
||||||
# assert (
|
== last_scan_data["samy"]["samy"].val
|
||||||
# plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"]
|
)
|
||||||
# == last_scan_data["samy"]["samy"].val
|
|
||||||
# )
|
|
||||||
|
def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
|
||||||
|
gui = connected_client_gui_obj
|
||||||
# def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
|
|
||||||
# gui = connected_client_gui_obj
|
assert gui.selected_device is None
|
||||||
|
assert len(gui.windows) == 1
|
||||||
# assert gui.selected_device is None
|
assert gui.windows["bec"] is gui.bec
|
||||||
# assert len(gui.windows) == 1
|
mw = gui.bec
|
||||||
# assert gui.windows["main"].widget is gui.main
|
assert mw.__class__.__name__ == "BECDockArea"
|
||||||
# assert gui.windows["main"].title == "BEC Widgets"
|
|
||||||
# mw = gui.main
|
xw = gui.new("X")
|
||||||
# assert mw.__class__.__name__ == "BECDockArea"
|
assert xw.__class__.__name__ == "BECDockArea"
|
||||||
|
assert len(gui.windows) == 2
|
||||||
# xw = gui.new("X")
|
|
||||||
# assert xw.__class__.__name__ == "BECDockArea"
|
gui_info = gui._dump()
|
||||||
# assert len(gui.windows) == 2
|
mw_info = gui_info[mw._gui_id]
|
||||||
|
assert mw_info["title"] == "BEC"
|
||||||
# gui_info = gui._dump()
|
assert mw_info["visible"]
|
||||||
# mw_info = gui_info[mw._gui_id]
|
xw_info = gui_info[xw._gui_id]
|
||||||
# assert mw_info["title"] == "BEC Widgets"
|
assert xw_info["title"] == "BEC - X"
|
||||||
# assert mw_info["visible"]
|
assert xw_info["visible"]
|
||||||
# xw_info = gui_info[xw._gui_id]
|
|
||||||
# assert xw_info["title"] == "X"
|
gui.hide()
|
||||||
# assert xw_info["visible"]
|
gui_info = gui._dump()
|
||||||
|
assert not any(windows["visible"] for windows in gui_info.values())
|
||||||
# gui.hide()
|
|
||||||
# gui_info = gui._dump()
|
gui.show()
|
||||||
# assert not any(windows["visible"] for windows in gui_info.values())
|
gui_info = gui._dump()
|
||||||
|
assert all(windows["visible"] for windows in gui_info.values())
|
||||||
# gui.show()
|
|
||||||
# gui_info = gui._dump()
|
assert gui._gui_is_alive()
|
||||||
# assert all(windows["visible"] for windows in gui_info.values())
|
gui._close()
|
||||||
|
assert not gui._gui_is_alive()
|
||||||
# assert gui.gui_is_alive()
|
gui._start_server(wait=True)
|
||||||
# gui.close()
|
assert gui._gui_is_alive()
|
||||||
# assert not gui.gui_is_alive()
|
# calling start multiple times should not change anything
|
||||||
# gui.start_server(wait=True)
|
gui._start_server(wait=True)
|
||||||
# assert gui.gui_is_alive()
|
gui._start()
|
||||||
# # calling start multiple times should not change anything
|
# gui.windows should have bec with gui_id 'bec'
|
||||||
# gui.start_server(wait=True)
|
assert len(gui.windows) == 1
|
||||||
# gui.start()
|
assert gui.windows["bec"]._gui_id == mw._gui_id
|
||||||
# # gui.windows should have main, and main dock area should have same gui_id as before
|
# communication should work, main dock area should have same id and be visible
|
||||||
# assert len(gui.windows) == 1
|
gui_info = gui._dump()
|
||||||
# assert gui.windows["main"].widget._gui_id == mw._gui_id
|
assert gui_info[mw._gui_id]["visible"]
|
||||||
# # communication should work, main dock area should have same id and be visible
|
|
||||||
# gui_info = gui._dump()
|
with pytest.raises(RuntimeError):
|
||||||
# assert gui_info[mw._gui_id]["visible"]
|
gui.bec.delete()
|
||||||
|
|
||||||
# with pytest.raises(RuntimeError):
|
yw = gui.new("Y")
|
||||||
# gui.main.delete()
|
assert len(gui.windows) == 2
|
||||||
|
yw.delete()
|
||||||
# yw = gui.new("Y")
|
assert len(gui.windows) == 1
|
||||||
# assert len(gui.windows) == 2
|
# check it is really deleted on server
|
||||||
# yw.delete()
|
gui_info = gui._dump()
|
||||||
# assert len(gui.windows) == 1
|
assert yw._gui_id not in gui_info
|
||||||
# # check it is really deleted on server
|
|
||||||
# gui_info = gui._dump()
|
|
||||||
# assert yw._gui_id not in gui_info
|
def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_obj, qtbot):
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
|
||||||
# def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_obj, qtbot):
|
gui.main.add_dock("test")
|
||||||
# gui = connected_client_gui_obj
|
qtbot.waitUntil(lambda: len(gui.main.panels) == 2) # default_figure + test
|
||||||
|
qtbot.wait(500)
|
||||||
# gui.main.add_dock("test")
|
with pytest.raises(ValueError):
|
||||||
# qtbot.waitUntil(lambda: len(gui.main.panels) == 2) # default_figure + test
|
gui.bec.add_dock("test")
|
||||||
# qtbot.wait(500)
|
# time.sleep(0.1)
|
||||||
# with pytest.raises(ValueError):
|
|
||||||
# gui.main.add_dock("test")
|
|
||||||
# # time.sleep(0.1)
|
|
||||||
|
@ -5,8 +5,11 @@ import pytest
|
|||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
|
||||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||||
|
from bec_widgets.cli.rpc.rpc_base import RPCReference
|
||||||
from bec_widgets.tests.utils import check_remote_data_size
|
from bec_widgets.tests.utils import check_remote_data_size
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def connected_figure(connected_client_gui_obj):
|
def connected_figure(connected_client_gui_obj):
|
||||||
@ -39,12 +42,15 @@ def test_rpc_plotting_shortcuts_init_configs(connected_figure, qtbot):
|
|||||||
|
|
||||||
# Checking if classes are correctly initialised
|
# Checking if classes are correctly initialised
|
||||||
assert len(fig.widgets) == 4
|
assert len(fig.widgets) == 4
|
||||||
assert plt.__class__.__name__ == "BECWaveform"
|
assert plt.__class__.__name__ == "RPCReference"
|
||||||
assert plt.__class__ == BECWaveform
|
assert plt.__class__ == RPCReference
|
||||||
assert im.__class__.__name__ == "BECImageShow"
|
assert plt._root._ipython_registry[plt._gui_id].__class__ == BECWaveform
|
||||||
assert im.__class__ == BECImageShow
|
assert im.__class__.__name__ == "RPCReference"
|
||||||
assert motor_map.__class__.__name__ == "BECMotorMap"
|
assert im.__class__ == RPCReference
|
||||||
assert motor_map.__class__ == BECMotorMap
|
assert im._root._ipython_registry[im._gui_id].__class__ == BECImageShow
|
||||||
|
assert motor_map.__class__.__name__ == "RPCReference"
|
||||||
|
assert motor_map.__class__ == RPCReference
|
||||||
|
assert motor_map._root._ipython_registry[motor_map._gui_id].__class__ == BECMotorMap
|
||||||
|
|
||||||
# check if the correct devices are set
|
# check if the correct devices are set
|
||||||
# plot
|
# plot
|
||||||
@ -215,10 +221,11 @@ def test_dap_rpc(connected_figure, bec_client_lib, qtbot):
|
|||||||
def test_removing_subplots(connected_figure, bec_client_lib):
|
def test_removing_subplots(connected_figure, bec_client_lib):
|
||||||
fig = connected_figure
|
fig = connected_figure
|
||||||
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||||
im = fig.image(monitor="eiger")
|
# Registry can't handle multiple subplots on one widget, BECFigure will be deprecated though
|
||||||
mm = fig.motor_map(motor_x="samx", motor_y="samy")
|
# im = fig.image(monitor="eiger")
|
||||||
|
# mm = fig.motor_map(motor_x="samx", motor_y="samy")
|
||||||
|
|
||||||
assert len(fig.widget_list) == 3
|
assert len(fig.widget_list) == 1
|
||||||
|
|
||||||
# removing curves
|
# removing curves
|
||||||
assert len(plt.curves) == 2
|
assert len(plt.curves) == 2
|
||||||
@ -229,7 +236,7 @@ def test_removing_subplots(connected_figure, bec_client_lib):
|
|||||||
|
|
||||||
# removing all subplots from figure
|
# removing all subplots from figure
|
||||||
plt.remove()
|
plt.remove()
|
||||||
im.remove()
|
# im.remove()
|
||||||
mm.remove()
|
# mm.remove()
|
||||||
|
|
||||||
assert len(fig.widget_list) == 0
|
assert len(fig.widget_list) == 0
|
||||||
|
Reference in New Issue
Block a user