1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-15 13:10:54 +02:00

Compare commits

..

11 Commits

12 changed files with 263 additions and 108 deletions

View File

@@ -6,7 +6,7 @@ image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
variables:
DOCKER_TLS_CERTDIR: ""
BEC_CORE_BRANCH: "main"
OPHYD_DEVICES_BRANCH: "master"
OPHYD_DEVICES_BRANCH: "main"
workflow:
rules:

View File

@@ -2,6 +2,19 @@
<!--next-version-placeholder-->
## v0.46.6 (2024-04-19)
### Fix
* **cli:** Fixed support for devices as cli input ([`1111610`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1111610f3206c5c46db6b4bd1e8827f1a4cd9e3f))
## v0.46.5 (2024-04-19)
### Fix
* **widgets/figure:** Individual cleanup disabled, making stuck rpc ([`ff52100`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ff52100e234debdfb5ccc0869352cfafde52ac93))
* **plots/waveform:** Colormap is correctly passed from BECFigure ([`026c079`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/026c0792bee25723013fffe57ccff10d9b652913))
## v0.46.4 (2024-04-16)
### Fix

View File

@@ -36,6 +36,17 @@ def rpc_call(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
# we could rely on a strict type check here, but this is more flexible
# moreover, it would anyway crash for objects...
out = []
for arg in args:
if hasattr(arg, "name"):
arg = arg.name
out.append(arg)
args = tuple(out)
for key, val in kwargs.items():
if hasattr(val, "name"):
kwargs[key] = val.name
if not self.gui_is_alive():
raise RuntimeError("GUI is not alive")
return self._run_rpc(func.__name__, *args, **kwargs)
@@ -82,8 +93,9 @@ def update_script(figure: BECFigure, msg):
print(f"Scan {scan_number} is running")
dev_x = scan_report_devices[0]
dev_y = scan_report_devices[1]
dev_z = get_selected_device(monitored_devices, figure.selected_device)
figure.clear_all()
plt = figure.plot(dev_x, dev_y, label=f"Scan {scan_number}")
plt = figure.plot(dev_x, dev_y, dev_z, label=f"Scan {scan_number}")
plt.set(title=f"Scan {scan_number}", x_label=dev_x, y_label=dev_y)
elif scan_report_devices:
dev_x = scan_report_devices[0]

View File

@@ -0,0 +1,102 @@
import os
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import uic
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils import BECDispatcher
from bec_widgets.widgets import BECFigure
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
def __init__(self):
super().__init__()
self.kernel_manager = QtInProcessKernelManager()
self.kernel_manager.start_kernel(show_banner=False)
self.kernel_client = self.kernel_manager.client()
self.kernel_client.start_channels()
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
# self.set_console_font_size(70)
def shutdown_kernel(self):
self.kernel_client.stop_channels()
self.kernel_manager.shutdown_kernel()
class JupyterConsoleWindow(QWidget): # pragma: no cover:
"""A widget that contains a Jupyter console linked to BEC Widgets with full API access (contains Qt and pyqtgraph API)."""
def __init__(self, parent=None):
super().__init__(parent)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "jupyter_console_window.ui"), self)
self._init_ui()
self.splitter.setSizes([200, 100])
self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close)
# console push
self.console.kernel_manager.kernel.shell.push(
{
"fig": self.figure,
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
}
)
def _init_ui(self):
# Plotting window
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
# add stuff to figure
self._init_figure()
self.console_layout = QVBoxLayout(self.widget_console)
self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console)
self.console.set_default_style("linux")
def _init_figure(self):
self.figure.plot("samx", "bpm4d")
self.figure.motor_map("samx", "samy")
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
self.figure.change_layout(2, 2)
self.w1 = self.figure[0, 0]
self.w2 = self.figure[0, 1]
self.w3 = self.figure[1, 0]
# curves for w1
self.w1.add_curve_scan("samx", "samy", "bpm4i", pen_style="dash")
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
self.c1 = self.w1.get_config()
if __name__ == "__main__": # pragma: no cover
import sys
bec_dispatcher = BECDispatcher()
client = bec_dispatcher.client
client.start()
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
win = JupyterConsoleWindow()
win.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,32 @@
from qtpy.QtCore import QUrl
from qtpy.QtWebEngineWidgets import QWebEngineView
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
class WebsiteWidget(QWidget):
def __init__(self, url):
super().__init__()
self.editor = QWebEngineView(self)
layout = QVBoxLayout()
layout.addWidget(self.editor)
self.setLayout(layout)
self.editor.setUrl(QUrl(url))
class VSCodeEditor(WebsiteWidget):
token = "bec"
host = "localhost"
port = 7000
def __init__(self):
super().__init__(f"http://{self.host}:{self.port}?tkn={self.token}")
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
mainWin = WebsiteWidget("https://scilog.psi.ch")
mainWin.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,59 @@
"""
Module to handle the vscode server
"""
import subprocess
class VSCodeServer:
"""
Class to handle the vscode server
"""
_instance = None
def __init__(self, port=7000, token="bec"):
self.started = False
self._server = None
self.port = port
self.token = token
def __new__(cls, *args, forced=False, **kwargs):
if cls._instance is None or forced:
cls._instance = super(VSCodeServer, cls).__new__(cls)
return cls._instance
def start_server(self):
"""
Start the vscode server in a subprocess
"""
if self.started:
return
self._server = subprocess.Popen(
f"code serve-web --port {self.port} --connection-token={self.token} --accept-server-license-terms",
shell=True,
)
self.started = True
def wait(self):
"""
Wait for the server to finish
"""
if not self.started:
return
if not self._server:
return
self._server.wait()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Start the vscode server")
parser.add_argument("--port", type=int, default=7000, help="Port to start the server")
parser.add_argument("--token", type=str, default="bec", help="Token to start the server")
args = parser.parse_args()
server = VSCodeServer(port=args.port, token=args.token)
server.start_server()
server.wait()

View File

@@ -227,7 +227,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
y_entry=y_entry,
z_entry=z_entry,
color=color,
color_map=color_map_z,
color_map_z=color_map_z,
label=label,
validate=validate,
)
@@ -313,7 +313,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
y_entry=y_entry,
z_entry=z_entry,
color=color,
color_map=color_map_z,
color_map_z=color_map_z,
label=label,
validate=validate,
)
@@ -787,8 +787,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
def clear_all(self):
"""Clear all widgets from the figure and reset to default state"""
for widget in self._widgets.values():
widget.cleanup()
# for widget in self._widgets.values():
# widget.cleanup()
self.clear()
self._widgets = defaultdict(dict)
self.grid = []
@@ -796,103 +796,3 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
self.config = FigureConfig(
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
##################################################
##################################################
# Debug window
##################################################
##################################################
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
def __init__(self):
super().__init__()
self.kernel_manager = QtInProcessKernelManager()
self.kernel_manager.start_kernel(show_banner=False)
self.kernel_client = self.kernel_manager.client()
self.kernel_client.start_channels()
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
# self.set_console_font_size(70)
def shutdown_kernel(self):
self.kernel_client.stop_channels()
self.kernel_manager.shutdown_kernel()
class DebugWindow(QWidget): # pragma: no cover:
"""Debug window for BEC widgets"""
def __init__(self, parent=None):
super().__init__(parent)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "figure_debug_minimal.ui"), self)
self._init_ui()
self.splitter.setSizes([200, 100])
self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close)
# console push
self.console.kernel_manager.kernel.shell.push(
{
"fig": self.figure,
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
}
)
def _init_ui(self):
# Plotting window
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
# add stuff to figure
self._init_figure()
self.console_layout = QVBoxLayout(self.widget_console)
self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console)
self.console.set_default_style("linux")
def _init_figure(self):
self.figure.plot("samx", "bpm4d")
self.figure.motor_map("samx", "samy")
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
self.figure.change_layout(2, 2)
self.w1 = self.figure[0, 0]
self.w2 = self.figure[0, 1]
self.w3 = self.figure[1, 0]
# curves for w1
self.w1.add_curve_scan("samx", "samy", "bpm4i", pen_style="dash")
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
self.c1 = self.w1.get_config()
if __name__ == "__main__": # pragma: no cover
import sys
bec_dispatcher = BECDispatcher()
client = bec_dispatcher.client
client.start()
app = QApplication(sys.argv)
win = DebugWindow()
win.show()
sys.exit(app.exec_())

View File

@@ -1,7 +1,7 @@
# pylint: disable= missing-module-docstring
from setuptools import find_packages, setup
__version__ = "0.46.4"
__version__ = "0.46.6"
# Default to PyQt6 if no other Qt binding is installed
QT_DEPENDENCY = "PyQt6>=6.0"

View File

@@ -42,9 +42,10 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger")
motor_map = fig.motor_map("samx", "samy")
plt_z = fig.add_plot("samx", "samy", "bpm4i")
# Checking if classes are correctly initialised
assert len(fig_server.widgets) == 3
assert len(fig_server.widgets) == 4
assert plt.__class__.__name__ == "BECWaveform"
assert plt.__class__ == BECWaveform
assert im.__class__.__name__ == "BECImageShow"
@@ -81,6 +82,13 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
},
"z": None,
}
# plot with z scatter
assert plt_z.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "samy", "entry": "samy", "unit": None, "modifier": None, "limits": None},
"z": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
}
def test_rpc_waveform_scan(rpc_server, qtbot):

View File

@@ -0,0 +1,29 @@
from unittest import mock
import pytest
from bec_widgets.cli.client import BECFigure
from .client_mocks import FakeDevice
@pytest.fixture
def cli_figure():
fig = BECFigure(gui_id="test")
with mock.patch.object(fig, "_run_rpc") as mock_rpc_call:
with mock.patch.object(fig, "gui_is_alive", return_value=True):
yield fig, mock_rpc_call
def test_rpc_call_plot(cli_figure):
fig, mock_rpc_call = cli_figure
fig.plot("samx", "bpm4i")
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")
def test_rpc_call_accepts_device_as_input(cli_figure):
dev1 = FakeDevice("samx")
dev2 = FakeDevice("bpm4i")
fig, mock_rpc_call = cli_figure
fig.plot(dev1, dev2)
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")