bec-widgets 0.49.1__py3-none-any.whl → 0.50.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bec_widgets/cli/client.py CHANGED
@@ -136,6 +136,13 @@ class BECPlotBase(RPCBase):
136
136
 
137
137
 
138
138
  class BECWaveform(RPCBase):
139
+ @property
140
+ @rpc_call
141
+ def rpc_id(self) -> "str":
142
+ """
143
+ Get the RPC ID of the widget.
144
+ """
145
+
139
146
  @property
140
147
  @rpc_call
141
148
  def config_dict(self) -> "dict":
@@ -387,6 +394,13 @@ class BECWaveform(RPCBase):
387
394
 
388
395
 
389
396
  class BECFigure(RPCBase, BECFigureClientMixin):
397
+ @property
398
+ @rpc_call
399
+ def rpc_id(self) -> "str":
400
+ """
401
+ Get the RPC ID of the widget.
402
+ """
403
+
390
404
  @property
391
405
  @rpc_call
392
406
  def config_dict(self) -> "dict":
@@ -396,13 +410,16 @@ class BECFigure(RPCBase, BECFigureClientMixin):
396
410
  dict: The configuration of the widget.
397
411
  """
398
412
 
399
- @property
400
413
  @rpc_call
401
- def axes(self) -> "list[BECPlotBase]":
414
+ def axes(self, row: "int", col: "int") -> "BECPlotBase":
402
415
  """
403
- Access all widget in BECFigure as a list
416
+ Get widget by its coordinates in the figure.
417
+ Args:
418
+ row(int): the row coordinate
419
+ col(int): the column coordinate
420
+
404
421
  Returns:
405
- list[BECPlotBase]: List of all widgets in the figure.
422
+ BECPlotBase: the widget at the given coordinates
406
423
  """
407
424
 
408
425
  @property
@@ -614,8 +631,36 @@ class BECFigure(RPCBase, BECFigureClientMixin):
614
631
  Clear all widgets from the figure and reset to default state
615
632
  """
616
633
 
634
+ @rpc_call
635
+ def get_all_rpc(self) -> "dict":
636
+ """
637
+ Get all registered RPC objects.
638
+ """
639
+
640
+ @property
641
+ @rpc_call
642
+ def widget_list(self) -> "list[BECPlotBase]":
643
+ """
644
+ Access all widget in BECFigure as a list
645
+ Returns:
646
+ list[BECPlotBase]: List of all widgets in the figure.
647
+ """
648
+
617
649
 
618
650
  class BECCurve(RPCBase):
651
+ @rpc_call
652
+ def remove(self):
653
+ """
654
+ Remove the curve from the plot.
655
+ """
656
+
657
+ @property
658
+ @rpc_call
659
+ def rpc_id(self) -> "str":
660
+ """
661
+ Get the RPC ID of the widget.
662
+ """
663
+
619
664
  @property
620
665
  @rpc_call
621
666
  def config_dict(self) -> "dict":
@@ -713,6 +758,13 @@ class BECCurve(RPCBase):
713
758
 
714
759
 
715
760
  class BECImageShow(RPCBase):
761
+ @property
762
+ @rpc_call
763
+ def rpc_id(self) -> "str":
764
+ """
765
+ Get the RPC ID of the widget.
766
+ """
767
+
716
768
  @property
717
769
  @rpc_call
718
770
  def config_dict(self) -> "dict":
@@ -1045,8 +1097,21 @@ class BECConnector(RPCBase):
1045
1097
  dict: The configuration of the widget.
1046
1098
  """
1047
1099
 
1100
+ @rpc_call
1101
+ def get_all_rpc(self) -> "dict":
1102
+ """
1103
+ Get all registered RPC objects.
1104
+ """
1105
+
1048
1106
 
1049
1107
  class BECImageItem(RPCBase):
1108
+ @property
1109
+ @rpc_call
1110
+ def rpc_id(self) -> "str":
1111
+ """
1112
+ Get the RPC ID of the widget.
1113
+ """
1114
+
1050
1115
  @property
1051
1116
  @rpc_call
1052
1117
  def config_dict(self) -> "dict":
@@ -157,17 +157,24 @@ class BECFigureClientMixin:
157
157
  self.stderr_output.clear()
158
158
 
159
159
  def _get_output(self) -> str:
160
- os.set_blocking(self._process.stdout.fileno(), False)
161
- os.set_blocking(self._process.stderr.fileno(), False)
162
- while self._process.poll() is None:
163
- readylist, _, _ = select.select([self._process.stdout, self._process.stderr], [], [], 1)
164
- if self._process.stdout in readylist:
165
- # print("*"*10, self._process.stdout.read(1024), flush=True, end="")
166
- self._process.stdout.read(1024)
167
- if self._process.stderr in readylist:
168
- # print("!"*10, self._process.stderr.read(1024), flush=True, end="", file=sys.stderr)
169
- print(self._process.stderr.read(1024), flush=True, end="", file=sys.stderr)
170
- self.stderr_output.append(self._process.stderr.read(1024))
160
+ try:
161
+ os.set_blocking(self._process.stdout.fileno(), False)
162
+ os.set_blocking(self._process.stderr.fileno(), False)
163
+ while self._process.poll() is None:
164
+ readylist, _, _ = select.select(
165
+ [self._process.stdout, self._process.stderr], [], [], 1
166
+ )
167
+ if self._process.stdout in readylist:
168
+ output = self._process.stdout.read(1024)
169
+ if output:
170
+ print(output, end="")
171
+ if self._process.stderr in readylist:
172
+ error_output = self._process.stderr.read(1024)
173
+ if error_output:
174
+ print(error_output, end="", file=sys.stderr)
175
+ self.stderr_output.append(error_output)
176
+ except Exception as e:
177
+ print(f"Error reading process output: {str(e)}")
171
178
 
172
179
 
173
180
  class RPCResponseTimeoutError(Exception):
@@ -0,0 +1,76 @@
1
+ from threading import Lock
2
+ from weakref import WeakValueDictionary
3
+
4
+ from qtpy.QtCore import QObject
5
+
6
+
7
+ class RPCRegister:
8
+ """
9
+ A singleton class that keeps track of all the RPC objects registered in the system for CLI usage.
10
+ """
11
+
12
+ _instance = None
13
+ _initialized = False
14
+ _lock = Lock()
15
+
16
+ def __new__(cls, *args, **kwargs):
17
+ if cls._instance is None:
18
+ cls._instance = super(RPCRegister, cls).__new__(cls)
19
+ cls._initialized = False
20
+ return cls._instance
21
+
22
+ def __init__(self):
23
+ if self._initialized:
24
+ return
25
+ self._rpc_register = WeakValueDictionary()
26
+ self._initialized = True
27
+
28
+ def add_rpc(self, rpc: QObject):
29
+ """
30
+ Add an RPC object to the register.
31
+ Args:
32
+ rpc(QObject): The RPC object to be added to the register.
33
+ """
34
+ if not hasattr(rpc, "gui_id"):
35
+ raise ValueError("RPC object must have a 'gui_id' attribute.")
36
+ self._rpc_register[rpc.gui_id] = rpc
37
+
38
+ def remove_rpc(self, rpc: str):
39
+ """
40
+ Remove an RPC object from the register.
41
+ Args:
42
+ rpc(str): The RPC object to be removed from the register.
43
+ """
44
+ if not hasattr(rpc, "gui_id"):
45
+ raise ValueError(f"RPC object {rpc} must have a 'gui_id' attribute.")
46
+ self._rpc_register.pop(rpc.gui_id, None)
47
+
48
+ def get_rpc_by_id(self, gui_id: str) -> QObject:
49
+ """
50
+ Get an RPC object by its ID.
51
+ Args:
52
+ gui_id(str): The ID of the RPC object to be retrieved.
53
+
54
+ Returns:
55
+ QObject: The RPC object with the given ID.
56
+ """
57
+ rpc_object = self._rpc_register.get(gui_id, None)
58
+ return rpc_object
59
+
60
+ def list_all_connections(self) -> dict:
61
+ """
62
+ List all the registered RPC objects.
63
+ Returns:
64
+ dict: A dictionary containing all the registered RPC objects.
65
+ """
66
+ with self._lock:
67
+ connections = dict(self._rpc_register)
68
+ return connections
69
+
70
+ @classmethod
71
+ def reset_singleton(cls):
72
+ """
73
+ Reset the singleton instance.
74
+ """
75
+ cls._instance = None
76
+ cls._initialized = False
bec_widgets/cli/server.py CHANGED
@@ -5,6 +5,7 @@ import time
5
5
  from bec_lib import MessageEndpoints, messages
6
6
  from qtpy.QtCore import QTimer
7
7
 
8
+ from bec_widgets.cli.rpc_register import RPCRegister
8
9
  from bec_widgets.utils import BECDispatcher
9
10
  from bec_widgets.utils.bec_connector import BECConnector
10
11
  from bec_widgets.widgets.figure import BECFigure
@@ -20,6 +21,8 @@ class BECWidgetsCLIServer:
20
21
  self.client.start()
21
22
  self.gui_id = gui_id
22
23
  self.fig = BECFigure(gui_id=self.gui_id)
24
+ self.rpc_register = RPCRegister()
25
+ self.rpc_register.add_rpc(self.fig)
23
26
 
24
27
  self.dispatcher.connect_slot(
25
28
  self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
@@ -34,10 +37,10 @@ class BECWidgetsCLIServer:
34
37
  def on_rpc_update(self, msg: dict, metadata: dict):
35
38
  request_id = metadata.get("request_id")
36
39
  try:
40
+ obj = self.get_object_from_config(msg["parameter"])
37
41
  method = msg["action"]
38
42
  args = msg["parameter"].get("args", [])
39
43
  kwargs = msg["parameter"].get("kwargs", {})
40
- obj = self.get_object_from_config(msg["parameter"])
41
44
  res = self.run_rpc(obj, method, args, kwargs)
42
45
  except Exception as e:
43
46
  print(e)
@@ -54,20 +57,10 @@ class BECWidgetsCLIServer:
54
57
 
55
58
  def get_object_from_config(self, config: dict):
56
59
  gui_id = config.get("gui_id")
57
- # check if the object is the figure
58
- if gui_id == self.fig.gui_id:
59
- return self.fig
60
- # check if the object is a widget
61
- if gui_id in self.fig._widgets:
62
- obj = self.fig._widgets[config["gui_id"]]
63
- return obj
64
- if self.fig._widgets:
65
- for widget in self.fig._widgets.values():
66
- item = widget.find_widget_by_id(gui_id)
67
- if item:
68
- return item
69
-
70
- raise ValueError(f"Object with gui_id {gui_id} not found")
60
+ obj = self.rpc_register.get_rpc_by_id(gui_id)
61
+ if obj is None:
62
+ raise ValueError(f"Object with gui_id {gui_id} not found")
63
+ return obj
71
64
 
72
65
  def run_rpc(self, obj, method, args, kwargs):
73
66
  method_obj = getattr(obj, method)
@@ -106,7 +99,6 @@ class BECWidgetsCLIServer:
106
99
  messages.StatusMessage(name=self.gui_id, status=1, info={}),
107
100
  expire=10,
108
101
  )
109
- print("Heartbeat emitted")
110
102
 
111
103
  def shutdown(self):
112
104
  self._shutdown_event = True
@@ -7,6 +7,7 @@ from qtconsole.inprocess import QtInProcessKernelManager
7
7
  from qtconsole.rich_jupyter_widget import RichJupyterWidget
8
8
  from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
9
9
 
10
+ from bec_widgets.cli.rpc_register import RPCRegister
10
11
  from bec_widgets.utils import BECDispatcher
11
12
  from bec_widgets.widgets import BECFigure
12
13
 
@@ -43,10 +44,14 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
43
44
  self.safe_close = False
44
45
  # self.figure.clean_signal.connect(self.confirm_close)
45
46
 
47
+ self.register = RPCRegister()
48
+ self.register.add_rpc(self.figure)
49
+ print("Registered objects:", dict(self.register.list_all_connections()))
46
50
  # console push
47
51
  self.console.kernel_manager.kernel.shell.push(
48
52
  {
49
53
  "fig": self.figure,
54
+ "register": self.register,
50
55
  "w1": self.w1,
51
56
  "w2": self.w2,
52
57
  "w3": self.w3,
@@ -7,6 +7,7 @@ from typing import Optional, Type
7
7
  from pydantic import BaseModel, Field, field_validator
8
8
  from qtpy.QtCore import Slot as pyqtSlot
9
9
 
10
+ from bec_widgets.cli.rpc_register import RPCRegister
10
11
  from bec_widgets.utils.bec_dispatcher import BECDispatcher
11
12
 
12
13
 
@@ -31,7 +32,7 @@ class ConnectionConfig(BaseModel):
31
32
  class BECConnector:
32
33
  """Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
33
34
 
34
- USER_ACCESS = ["config_dict"]
35
+ USER_ACCESS = ["config_dict", "get_all_rpc"]
35
36
 
36
37
  def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
37
38
  # BEC related connections
@@ -54,6 +55,25 @@ class BECConnector:
54
55
  else:
55
56
  self.gui_id = self.config.gui_id
56
57
 
58
+ # register widget to rpc register
59
+ self.rpc_register = RPCRegister()
60
+ self.rpc_register.add_rpc(self)
61
+
62
+ def get_all_rpc(self) -> dict:
63
+ """Get all registered RPC objects."""
64
+ all_connections = self.rpc_register.list_all_connections()
65
+ return dict(all_connections)
66
+
67
+ @property
68
+ def rpc_id(self) -> str:
69
+ """Get the RPC ID of the widget."""
70
+ return self.gui_id
71
+
72
+ @rpc_id.setter
73
+ def rpc_id(self, rpc_id: str) -> None:
74
+ """Set the RPC ID of the widget."""
75
+ self.gui_id = rpc_id
76
+
57
77
  @property
58
78
  def config_dict(self) -> dict:
59
79
  """
@@ -97,6 +97,7 @@ class WidgetHandler:
97
97
 
98
98
  class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
99
99
  USER_ACCESS = [
100
+ "rpc_id",
100
101
  "config_dict",
101
102
  "axes",
102
103
  "widgets",
@@ -110,6 +111,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
110
111
  "change_layout",
111
112
  "change_theme",
112
113
  "clear_all",
114
+ "get_all_rpc",
115
+ "widget_list",
113
116
  ]
114
117
 
115
118
  clean_signal = pyqtSignal()
@@ -138,8 +141,21 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
138
141
  # Container to keep track of the grid
139
142
  self.grid = []
140
143
 
144
+ def __getitem__(self, key: tuple | str):
145
+ if isinstance(key, tuple) and len(key) == 2:
146
+ return self.axes(*key)
147
+ elif isinstance(key, str):
148
+ widget = self._widgets.get(key)
149
+ if widget is None:
150
+ raise KeyError(f"No widget with ID {key}")
151
+ return self._widgets.get(key)
152
+ else:
153
+ raise TypeError(
154
+ "Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
155
+ )
156
+
141
157
  @property
142
- def axes(self) -> list[BECPlotBase]:
158
+ def widget_list(self) -> list[BECPlotBase]:
143
159
  """
144
160
  Access all widget in BECFigure as a list
145
161
  Returns:
@@ -148,8 +164,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
148
164
  axes = [value for value in self._widgets.values() if isinstance(value, BECPlotBase)]
149
165
  return axes
150
166
 
151
- @axes.setter
152
- def axes(self, value: list[BECPlotBase]):
167
+ @widget_list.setter
168
+ def widget_list(self, value: list[BECPlotBase]):
153
169
  self._axes = value
154
170
 
155
171
  @property
@@ -623,7 +639,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
623
639
  row(int): The row coordinate of the widget to remove.
624
640
  col(int): The column coordinate of the widget to remove.
625
641
  """
626
- widget = self._get_widget_by_coordinates(row, col)
642
+ widget = self.axes(row, col)
627
643
  if widget:
628
644
  widget_id = widget.config.gui_id
629
645
  if widget_id in self._widgets:
@@ -643,24 +659,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
643
659
  self._reindex_grid()
644
660
  if widget_id in self.config.widgets:
645
661
  self.config.widgets.pop(widget_id)
646
- print(f"Removed widget {widget_id}.")
647
662
  else:
648
663
  raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
649
664
 
650
- def __getitem__(self, key: tuple | str):
651
- if isinstance(key, tuple) and len(key) == 2:
652
- return self._get_widget_by_coordinates(*key)
653
- elif isinstance(key, str):
654
- widget = self._widgets.get(key)
655
- if widget is None:
656
- raise KeyError(f"No widget with ID {key}")
657
- return self._widgets.get(key)
658
- else:
659
- raise TypeError(
660
- "Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
661
- )
662
-
663
- def _get_widget_by_coordinates(self, row: int, col: int) -> BECPlotBase:
665
+ def axes(self, row: int, col: int) -> BECPlotBase:
664
666
  """
665
667
  Get widget by its coordinates in the figure.
666
668
  Args:
@@ -699,7 +701,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
699
701
 
700
702
  def _reindex_grid(self):
701
703
  """Reindex the grid to remove empty rows and columns."""
702
- print(f"old grid: {self.grid}")
703
704
  new_grid = []
704
705
  for row in self.grid:
705
706
  new_row = [widget for widget in row if widget is not None]
@@ -766,8 +767,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
766
767
 
767
768
  def clear_all(self):
768
769
  """Clear all widgets from the figure and reset to default state"""
769
- # for widget in self._widgets.values():
770
- # widget.cleanup()
770
+ for widget in self._widgets.values():
771
+ widget.cleanup()
771
772
  self.clear()
772
773
  self._widgets = defaultdict(dict)
773
774
  self.grid = []
@@ -59,6 +59,7 @@ class ImageConfig(WidgetConfig):
59
59
 
60
60
  class BECImageItem(BECConnector, pg.ImageItem):
61
61
  USER_ACCESS = [
62
+ "rpc_id",
62
63
  "config_dict",
63
64
  "set",
64
65
  "set_fft",
@@ -287,9 +288,14 @@ class BECImageItem(BECConnector, pg.ImageItem):
287
288
  else:
288
289
  raise ValueError("style should be 'simple' or 'full'")
289
290
 
291
+ def cleanup(self):
292
+ """Clean up widget."""
293
+ self.rpc_register.remove_rpc(self)
294
+
290
295
 
291
296
  class BECImageShow(BECPlotBase):
292
297
  USER_ACCESS = [
298
+ "rpc_id",
293
299
  "config_dict",
294
300
  "add_image_by_config",
295
301
  "get_image_config",
@@ -358,20 +364,6 @@ class BECImageShow(BECPlotBase):
358
364
 
359
365
  thread.start()
360
366
 
361
- def find_widget_by_id(self, item_id: str) -> BECImageItem:
362
- """
363
- Find the widget by its gui_id.
364
- Args:
365
- item_id(str): The gui_id of the widget.
366
-
367
- Returns:
368
- BECImageItem: The widget with the given gui_id.
369
- """
370
- for source, images in self._images.items():
371
- for monitor, image_item in images.items():
372
- if image_item.gui_id == item_id:
373
- return image_item
374
-
375
367
  def find_image_by_monitor(self, item_id: str) -> BECImageItem:
376
368
  """
377
369
  Find the widget by its gui_id.
@@ -719,10 +711,8 @@ class BECImageShow(BECPlotBase):
719
711
  processing_config = image_to_update.config.processing
720
712
  self.processor.set_config(processing_config)
721
713
  if self.use_threading:
722
- print("using threaded version")
723
714
  self._create_thread_worker(device, data)
724
715
  else:
725
- print("using NON-threaded version")
726
716
  data = self.processor.process_image(data)
727
717
  self.update_image(device, data)
728
718
 
@@ -809,14 +799,14 @@ class BECImageShow(BECPlotBase):
809
799
  """
810
800
  Clean up the widget.
811
801
  """
812
- print(f"Cleaning up {self.gui_id}")
813
- # for monitor in self._images["device_monitor"]:
814
- # self.bec_dispatcher.disconnect_slot(
815
- # self.on_image_update, MessageEndpoints.device_monitor(monitor)
816
- # )
817
- # if self.thread is not None and self.thread.isRunning():
818
- # self.thread.quit()
819
- # self.thread.wait()
802
+ for monitor in self._images["device_monitor"]:
803
+ self.bec_dispatcher.disconnect_slot(
804
+ self.on_image_update, MessageEndpoints.device_monitor(monitor)
805
+ )
806
+ for image in self.images:
807
+ image.cleanup()
808
+
809
+ self.rpc_register.remove_rpc(self)
820
810
 
821
811
 
822
812
  class ImageProcessor:
@@ -182,14 +182,18 @@ class BECMotorMap(BECPlotBase):
182
182
  """
183
183
  self.config.scatter_size = scatter_size
184
184
 
185
- def _connect_motor_to_slots(self):
186
- """Connect motors to slots."""
185
+ def _disconnect_current_motors(self):
186
+ """Disconnect the current motors from the slots."""
187
187
  if self.motor_x is not None and self.motor_y is not None:
188
- old_endpoints = [
188
+ endpoints = [
189
189
  MessageEndpoints.device_readback(self.motor_x),
190
190
  MessageEndpoints.device_readback(self.motor_y),
191
191
  ]
192
- self.bec_dispatcher.disconnect_slot(self.on_device_readback, old_endpoints)
192
+ self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints)
193
+
194
+ def _connect_motor_to_slots(self):
195
+ """Connect motors to slots."""
196
+ self._disconnect_current_motors()
193
197
 
194
198
  self.motor_x = self.config.signals.x.name
195
199
  self.motor_y = self.config.signals.y.name
@@ -418,18 +422,7 @@ class BECMotorMap(BECPlotBase):
418
422
 
419
423
  self.update_signal.emit()
420
424
 
421
-
422
- if __name__ == "__main__": # pragma: no cover
423
- import sys
424
-
425
- import pyqtgraph as pg
426
- from qtpy.QtWidgets import QApplication
427
-
428
- app = QApplication(sys.argv)
429
- glw = pg.GraphicsLayoutWidget()
430
- motor_map = BECMotorMap()
431
- motor_map.change_motors("samx", "samy")
432
- glw.addItem(motor_map)
433
- widget = glw
434
- widget.show()
435
- sys.exit(app.exec_())
425
+ def cleanup(self):
426
+ """Cleanup the widget."""
427
+ self._disconnect_current_motors()
428
+ self.rpc_register.remove_rpc(self)
@@ -243,6 +243,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
243
243
  def remove(self):
244
244
  """Remove the plot widget from the figure."""
245
245
  if self.figure is not None:
246
+ self.cleanup()
246
247
  self.figure.remove(widget_id=self.gui_id)
247
248
 
248
249
  def cleanup(self):
@@ -64,6 +64,8 @@ class Waveform1DConfig(WidgetConfig):
64
64
 
65
65
  class BECCurve(BECConnector, pg.PlotDataItem):
66
66
  USER_ACCESS = [
67
+ "remove",
68
+ "rpc_id",
67
69
  "config_dict",
68
70
  "set",
69
71
  "set_data",
@@ -82,6 +84,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
82
84
  name: Optional[str] = None,
83
85
  config: Optional[CurveConfig] = None,
84
86
  gui_id: Optional[str] = None,
87
+ parent_item: Optional[pg.PlotItem] = None,
85
88
  **kwargs,
86
89
  ):
87
90
  if config is None:
@@ -93,6 +96,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
93
96
  super().__init__(config=config, gui_id=gui_id)
94
97
  pg.PlotDataItem.__init__(self, name=name)
95
98
 
99
+ self.parent_item = parent_item
96
100
  self.apply_config()
97
101
  if kwargs:
98
102
  self.set(**kwargs)
@@ -225,9 +229,19 @@ class BECCurve(BECConnector, pg.PlotDataItem):
225
229
  x_data, y_data = self.getData()
226
230
  return x_data, y_data
227
231
 
232
+ def cleanup(self):
233
+ """Cleanup the curve."""
234
+ self.rpc_register.remove_rpc(self)
235
+
236
+ def remove(self):
237
+ """Remove the curve from the plot."""
238
+ self.cleanup()
239
+ self.parent_item.removeItem(self)
240
+
228
241
 
229
242
  class BECWaveform(BECPlotBase):
230
243
  USER_ACCESS = [
244
+ "rpc_id",
231
245
  "config_dict",
232
246
  "add_curve_scan",
233
247
  "add_curve_custom",
@@ -286,19 +300,6 @@ class BECWaveform(BECPlotBase):
286
300
  self.add_legend()
287
301
  self.apply_config(self.config)
288
302
 
289
- def find_widget_by_id(self, item_id: str) -> BECCurve:
290
- """
291
- Find the curve by its ID.
292
- Args:
293
- item_id(str): ID of the curve.
294
-
295
- Returns:
296
- BECCurve: The curve object.
297
- """
298
- for curve in self.plot_item.curves:
299
- if curve.gui_id == item_id:
300
- return curve
301
-
302
303
  def apply_config(self, config: dict | WidgetConfig, replot_last_scan: bool = False):
303
304
  """
304
305
  Apply the configuration to the 1D waveform widget.
@@ -468,7 +469,7 @@ class BECWaveform(BECPlotBase):
468
469
  Returns:
469
470
  BECCurve: The curve object.
470
471
  """
471
- curve = BECCurve(config=config, name=name)
472
+ curve = BECCurve(config=config, name=name, parent_item=self.plot_item)
472
473
  self._curves_data[source][name] = curve
473
474
  self.plot_item.addItem(curve)
474
475
  self.config.curves[name] = curve.config
@@ -796,3 +797,6 @@ class BECWaveform(BECPlotBase):
796
797
  def cleanup(self):
797
798
  """Cleanup the widget connection from BECDispatcher."""
798
799
  self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
800
+ for curve in self.curves:
801
+ curve.cleanup()
802
+ self.rpc_register.remove_rpc(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bec-widgets
3
- Version: 0.49.1
3
+ Version: 0.50.0
4
4
  Summary: BEC Widgets
5
5
  Home-page: https://gitlab.psi.ch/bec/bec-widgets
6
6
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec-widgets/issues
@@ -2,16 +2,17 @@ bec_widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  bec_widgets/cli/__init__.py,sha256=ULoNTVnv2UeSSjhFq3uCQJ-0JTJf9oU76l27aRiizL4,78
3
3
  bec_widgets/cli/auto_updates.py,sha256=ptZeBKr13o9THc8oKLn93K_16i6G3pxzw8hZ4MUgjW4,3845
4
4
  bec_widgets/cli/bec_widgets_icon.png,sha256=pRCGpoOtwyZl97fBV_CHcGppliErzd0qQkCXLxjbp-s,5760
5
- bec_widgets/cli/client.py,sha256=4QedDVBgkCOnRQFYQcAabyAsl5UQ0aRwwLwk1OR5M1k,37882
6
- bec_widgets/cli/client_utils.py,sha256=B5NMKhccUS_Vavi0klW_aEdItfZTwkM-cOLGbbdjBc4,9978
5
+ bec_widgets/cli/client.py,sha256=AvkaEDKB8cZFm2WS5JWIusMXtcqErEeP2Ayk9l7iAp8,39163
6
+ bec_widgets/cli/client_utils.py,sha256=FBuU0LTi1lQrzRIH_4ued_isC_iknYMAaNxnSTsJvTw,10118
7
7
  bec_widgets/cli/generate_cli.py,sha256=JLqUlUgfz_f_4KHPRUAN-Xli-K7uNOc8-F-LkAC7Scw,4004
8
- bec_widgets/cli/server.py,sha256=UDD9HmiNOM94LvewRsNWvXDA5-ZJ9eWsCIk3_nIHesw,5034
8
+ bec_widgets/cli/rpc_register.py,sha256=OZOWX0IKGXqDsnrUYi0Irl_zpPS4Q_JPCV9JQfN6YYw,2212
9
+ bec_widgets/cli/server.py,sha256=Xru-gLqDhTRIu6EnYIsFXeinMDW_NbtKYtPivlfdBY8,4772
9
10
  bec_widgets/examples/__init__.py,sha256=WWQ0cu7m8sA4Ehy-DWdTIqSISjaHsbxhsNmNrMnhDZU,202
10
11
  bec_widgets/examples/eiger_plot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
12
  bec_widgets/examples/eiger_plot/eiger_plot.py,sha256=Uxl2Usf8jEzaX7AT8zVqa1x8ZIEgI1HmazSlb-tRFWE,10359
12
13
  bec_widgets/examples/eiger_plot/eiger_plot.ui,sha256=grHfnO3OG_lelJhdRsnA0badCvRdDunPrIMIyNQ5N-w,5809
13
14
  bec_widgets/examples/jupyter_console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- bec_widgets/examples/jupyter_console/jupyter_console_window.py,sha256=MW9PCM4-UileXxMMDV2LxUsdxdTf2zPavhA4QxqoBxk,3323
15
+ bec_widgets/examples/jupyter_console/jupyter_console_window.py,sha256=aEwwOHNW4CJab41hbEB3hvWEEAPNtGjFQEwMo8GkEmM,3581
15
16
  bec_widgets/examples/jupyter_console/jupyter_console_window.ui,sha256=GodXBvBvs5QAUsHbo3pcxR4o51Tvce4DTqpTluk3hOs,742
16
17
  bec_widgets/examples/mca_readout/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  bec_widgets/examples/mca_readout/mca_plot.py,sha256=do7mSK_nzHtojRiMi8JoN_Rckg9yfjYYWz2S_Nl3xbE,5079
@@ -28,7 +29,7 @@ bec_widgets/examples/stream_plot/line_plot.ui,sha256=rgNfhOXu1AcWF0P6wnOlmJKDjS-
28
29
  bec_widgets/examples/stream_plot/stream_plot.py,sha256=vHii1p9JxSyGQ_VcCjnk9SHJ41Q6Oi1GGd6swVVHLRM,12177
29
30
  bec_widgets/simulations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  bec_widgets/utils/__init__.py,sha256=xytx86Yosjkta0PU4rHfoeO7FCPcimS15xjMPQUgIXc,403
31
- bec_widgets/utils/bec_connector.py,sha256=j3RfI09ZHWhWy01cRmIGKrz6GF6L07_gCMAw4Cr9jkQ,4237
32
+ bec_widgets/utils/bec_connector.py,sha256=U_quQy7p1ISEpTnvwKsnDw5rdCc3jEoATfPVez2K7eI,4867
32
33
  bec_widgets/utils/bec_dispatcher.py,sha256=sLv9CmJ3GKGDhvCXCDmuKtNRlI4w1oWxuQu_Mq2mDDY,4840
33
34
  bec_widgets/utils/bec_table.py,sha256=Xy5qM343K8EvEpB4g_129b63yo1wdEvEY3wqxB_p_Iw,716
34
35
  bec_widgets/utils/colors.py,sha256=JsLxzkxbw-I8GIuvnIKyiM83n0edhyMG2Fa4Ffm62ww,2392
@@ -45,7 +46,7 @@ bec_widgets/validation/__init__.py,sha256=ismd1bU5FhFb0zFPwNKuq7oT48G4Y2GfaMZOdN
45
46
  bec_widgets/validation/monitor_config_validator.py,sha256=M9p8K_nvxicnqJB4X7j90R377WHYVH4wMCtSXsRI51M,8150
46
47
  bec_widgets/widgets/__init__.py,sha256=HBzIWJYX4dp2iDZl_qIuyy-X5IWRMhGwQ-4UisP8wqE,353
47
48
  bec_widgets/widgets/figure/__init__.py,sha256=3hGx_KOV7QHCYAV06aNuUgKq4QIYCjUTad-DrwkUaBM,44
48
- bec_widgets/widgets/figure/figure.py,sha256=55Dc3DwdeC4rBDz9KLF6udfQJjnDuLQ-1QJ5oOF4Quw,28559
49
+ bec_widgets/widgets/figure/figure.py,sha256=IOj5rz3dXHOThtdLRPHx0lpoIQCLg-QbPGO1ECM-CKo,28484
49
50
  bec_widgets/widgets/monitor/__init__.py,sha256=afXuZcBOxNAuYdCkIQXX5J60R5A3Q_86lNEW2vpFtPI,32
50
51
  bec_widgets/widgets/monitor/config_dialog.py,sha256=Z1a4WRIVlfEGdwC-QG25kba2EHCZWi5J843tBVZlWiI,20275
51
52
  bec_widgets/widgets/monitor/config_dialog.ui,sha256=ISMcF7CLTAMXhfZh2Yv5yezzAjMtb9fxY1pmX4B_jCg,5932
@@ -60,18 +61,22 @@ bec_widgets/widgets/motor_control/motor_control_table.ui,sha256=t6aRKiSmutMfp0Ay
60
61
  bec_widgets/widgets/motor_map/__init__.py,sha256=K3c-3A_LbxK0UJ0_bV3opL-wGLTwBLendsJXsg8GAqE,32
61
62
  bec_widgets/widgets/motor_map/motor_map.py,sha256=vJlLWa0BXv5KMZ7UVT0q3I1I5CXgsDiXoM_ptsAsG3c,21860
62
63
  bec_widgets/widgets/plots/__init__.py,sha256=kGQTORTr-2M9vmVCK-T7AFre4bY5LVVzGxcIzT81-ZU,237
63
- bec_widgets/widgets/plots/image.py,sha256=jgKl9BVu9FGqCHkij4gbV12pYugTwyQVhd-Y_TXEvbE,33005
64
- bec_widgets/widgets/plots/motor_map.py,sha256=Uitx080FEyCDCTYHfBt0Ah-98QD4J5nSNZ628lu7EOg,15386
65
- bec_widgets/widgets/plots/plot_base.py,sha256=xuNA4S5lwWiYXHT60XklWX09KiwqbrRiDr4J_iDXkes,8447
66
- bec_widgets/widgets/plots/waveform.py,sha256=aUaWPg5NL0HoGqEs3Yo5nXEg_qy31C5ZwaOAwIoiqcs,28528
64
+ bec_widgets/widgets/plots/image.py,sha256=uanFs3YoAefLgfQnAW5oiQtssRLfhKmWHMlOMpb9HqQ,32504
65
+ bec_widgets/widgets/plots/motor_map.py,sha256=wbD95aIA4K3BLDZHZmHLblvKea7jPA7u4yw_tFaA4OE,15298
66
+ bec_widgets/widgets/plots/plot_base.py,sha256=ZieahcTwz0tynszdYe4r0rrOlWcRmXblplIYuuTX1ic,8474
67
+ bec_widgets/widgets/plots/waveform.py,sha256=CzPPq_9ZgBr4pZkLNSDqPbP8AJXXkan221-Fe8rw31c,28693
67
68
  bec_widgets/widgets/scan_control/__init__.py,sha256=IOfHl15vxb_uC6KN62-PeUzbBha_vQyqkkXbJ2HU674,38
68
69
  bec_widgets/widgets/scan_control/scan_control.py,sha256=tbO9tbVynRvs4VCxTZ4ZFBDTVAojIr-zkl70vuHbWgw,17116
69
70
  bec_widgets/widgets/toolbar/__init__.py,sha256=d-TP4_cr_VbpwreMM4ePnfZ5YXsEPQ45ibEf75nuGoE,36
70
71
  bec_widgets/widgets/toolbar/toolbar.py,sha256=sxz7rbc8XNPS6n2WMObF4-2PqdYfPxVtsOZEGV6mqa0,5124
71
72
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
+ tests/end-2-end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ tests/end-2-end/conftest.py,sha256=jksYsc3Vm15LDyke-fmc80qCKBUnYuEjYi_QikeSOkE,756
75
+ tests/end-2-end/test_bec_figure_rpc_e2e.py,sha256=-eMCKkj6sh_OkuVRhVTGMw5KJOyNhzDR783jdi5hNM4,5350
76
+ tests/end-2-end/test_rpc_register_e2e.py,sha256=zqatLWjM_tYxiB3GgupYHHGTorGUFa0jvDFaqJpkZyA,1560
72
77
  tests/unit_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
78
  tests/unit_tests/client_mocks.py,sha256=LNUgI9Ccv5Ol7_pmybIhoVqZZem1RPIsTDk7ZTARNls,4128
74
- tests/unit_tests/conftest.py,sha256=roLbKZ1thm2Bd-5zEtL-eRBB5TTs36sAqXTUdHYYqSw,433
79
+ tests/unit_tests/conftest.py,sha256=KrnktXPWmZhnKNue-xGWOLD1XGEvdz9Vf7V2eO3XQ3A,596
75
80
  tests/unit_tests/test_bec_connector.py,sha256=f2XXGGw3NoZLIUrDuZuEWwF_ttOYmmquCgUrV5XkIOY,1951
76
81
  tests/unit_tests/test_bec_dispatcher.py,sha256=MtNyfC7-Y4na-Fwf1ny9raHBqE45eSnQNWSqqAx79FU,1857
77
82
  tests/unit_tests/test_bec_figure.py,sha256=T4k-E1D3sjTTDTFZGdTFDQv0EYNQ_R-QbWOM7pQwFw4,7926
@@ -85,6 +90,7 @@ tests/unit_tests/test_generate_cli_client.py,sha256=BdpTZMNUFOBJa2e-rme9AJUoXfue
85
90
  tests/unit_tests/test_motor_control.py,sha256=jdTG35z3jOL9XCAIDNIGfdv60vcwGLHa3KJjKqJkoZw,20322
86
91
  tests/unit_tests/test_motor_map.py,sha256=UEjmtIYI2mxq9BUeopqoqNNy7UiPJEts9h45ufsFcrA,5979
87
92
  tests/unit_tests/test_plot_base.py,sha256=bOdlgAxh9oKk5PwiQ_MSFmzr44uJ61Tlg242RCIhl5c,2610
93
+ tests/unit_tests/test_rpc_register.py,sha256=hECjZEimd440mwRrO0rg7L3PKN7__3DgjmESN6wx3bo,1179
88
94
  tests/unit_tests/test_scan_control.py,sha256=7dtGpE0g4FqUhhQeCkyJl-9o7NH3DFZJgEaqDmBYbBc,7551
89
95
  tests/unit_tests/test_stream_plot.py,sha256=LNCYIj9CafremGaz-DwDktCRJRrjgfOdVewCUwwZE5s,5843
90
96
  tests/unit_tests/test_validator_errors.py,sha256=NFxyv0TIOXeZKZRRUBfVQ7bpunwY4KkG95yTUdQmvns,3532
@@ -93,8 +99,8 @@ tests/unit_tests/test_widget_io.py,sha256=FeL3ZYSBQnRt6jxj8VGYw1cmcicRQyHKleahw7
93
99
  tests/unit_tests/test_yaml_dialog.py,sha256=HNrqferkdg02-9ieOhhI2mr2Qvt7GrYgXmQ061YCTbg,5794
94
100
  tests/unit_tests/test_msgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
101
  tests/unit_tests/test_msgs/available_scans_message.py,sha256=m_z97hIrjHXXMa2Ex-UvsPmTxOYXfjxyJaGkIY6StTY,46532
96
- bec_widgets-0.49.1.dist-info/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
97
- bec_widgets-0.49.1.dist-info/METADATA,sha256=zNfCnStMfaGxDZGKjyISa0HkT057GKdMEwVWKIjONGU,3719
98
- bec_widgets-0.49.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
99
- bec_widgets-0.49.1.dist-info/top_level.txt,sha256=EXCwhJYmXmd1DjYYL3hrGsddX-97IwYSiIHrf27FFVk,18
100
- bec_widgets-0.49.1.dist-info/RECORD,,
102
+ bec_widgets-0.50.0.dist-info/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
103
+ bec_widgets-0.50.0.dist-info/METADATA,sha256=2i8dBEDDsAAbm4Hgxf_Vt5h2weKOK-dQ0KKtb75wlG0,3719
104
+ bec_widgets-0.50.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
105
+ bec_widgets-0.50.0.dist-info/top_level.txt,sha256=EXCwhJYmXmd1DjYYL3hrGsddX-97IwYSiIHrf27FFVk,18
106
+ bec_widgets-0.50.0.dist-info/RECORD,,
File without changes
@@ -0,0 +1,25 @@
1
+ import pytest
2
+
3
+ from bec_widgets.cli.rpc_register import RPCRegister
4
+ from bec_widgets.cli.server import BECWidgetsCLIServer
5
+ from bec_widgets.utils import BECDispatcher
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def rpc_register():
10
+ yield RPCRegister()
11
+ RPCRegister.reset_singleton()
12
+
13
+
14
+ @pytest.fixture
15
+ def rpc_server(qtbot, bec_client_lib, threads_check):
16
+ dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
17
+ server = BECWidgetsCLIServer(gui_id="figure")
18
+ qtbot.addWidget(server.fig)
19
+ qtbot.waitExposed(server.fig)
20
+ qtbot.wait(1000) # 1s long to wait until gui is ready
21
+ yield server
22
+ dispatcher.disconnect_all()
23
+ server.client.shutdown()
24
+ server.shutdown()
25
+ dispatcher.reset_singleton()
@@ -0,0 +1,165 @@
1
+ import numpy as np
2
+ import pytest
3
+ from bec_lib import MessageEndpoints
4
+
5
+ from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
6
+
7
+
8
+ def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
9
+ fig = BECFigure(rpc_server.gui_id)
10
+ fig_server = rpc_server.fig
11
+
12
+ ax = fig.add_plot()
13
+ curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
14
+ curve.set_color("red")
15
+ curve = ax.curves[0]
16
+ curve.set_color("blue")
17
+
18
+ assert len(fig_server.widgets) == 1
19
+ assert len(fig_server.widgets["widget_1"].curves) == 1
20
+
21
+
22
+ def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
23
+ fig = BECFigure(rpc_server.gui_id)
24
+ fig_server = rpc_server.fig
25
+
26
+ plt = fig.plot("samx", "bpm4i")
27
+ im = fig.image("eiger")
28
+ motor_map = fig.motor_map("samx", "samy")
29
+ plt_z = fig.add_plot("samx", "samy", "bpm4i")
30
+
31
+ # Checking if classes are correctly initialised
32
+ assert len(fig_server.widgets) == 4
33
+ assert plt.__class__.__name__ == "BECWaveform"
34
+ assert plt.__class__ == BECWaveform
35
+ assert im.__class__.__name__ == "BECImageShow"
36
+ assert im.__class__ == BECImageShow
37
+ assert motor_map.__class__.__name__ == "BECMotorMap"
38
+ assert motor_map.__class__ == BECMotorMap
39
+
40
+ # check if the correct devices are set
41
+ # plot
42
+ assert plt.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
43
+ "source": "scan_segment",
44
+ "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
45
+ "y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
46
+ "z": None,
47
+ }
48
+ # image
49
+ assert im.config_dict["images"]["eiger"]["monitor"] == "eiger"
50
+ # motor map
51
+ assert motor_map.config_dict["signals"] == {
52
+ "source": "device_readback",
53
+ "x": {
54
+ "name": "samx",
55
+ "entry": "samx",
56
+ "unit": None,
57
+ "modifier": None,
58
+ "limits": [-50.0, 50.0],
59
+ },
60
+ "y": {
61
+ "name": "samy",
62
+ "entry": "samy",
63
+ "unit": None,
64
+ "modifier": None,
65
+ "limits": [-50.0, 50.0],
66
+ },
67
+ "z": None,
68
+ }
69
+ # plot with z scatter
70
+ assert plt_z.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
71
+ "source": "scan_segment",
72
+ "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
73
+ "y": {"name": "samy", "entry": "samy", "unit": None, "modifier": None, "limits": None},
74
+ "z": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
75
+ }
76
+
77
+
78
+ def test_rpc_waveform_scan(rpc_server, qtbot):
79
+ fig = BECFigure(rpc_server.gui_id)
80
+
81
+ # add 3 different curves to track
82
+ plt = fig.plot("samx", "bpm4i")
83
+ fig.plot("samx", "bpm3a")
84
+ fig.plot("samx", "bpm4d")
85
+
86
+ client = rpc_server.client
87
+ dev = client.device_manager.devices
88
+ scans = client.scans
89
+ queue = client.queue
90
+
91
+ status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
92
+
93
+ # wait for scan to finish
94
+ while not status.status == "COMPLETED":
95
+ qtbot.wait(200)
96
+
97
+ last_scan_data = queue.scan_storage.storage[-1].data
98
+
99
+ # get data from curves
100
+ plt_data = plt.get_all_data()
101
+
102
+ # check plotted data
103
+ assert plt_data["bpm4i-bpm4i"]["x"] == last_scan_data["samx"]["samx"].val
104
+ assert plt_data["bpm4i-bpm4i"]["y"] == last_scan_data["bpm4i"]["bpm4i"].val
105
+ assert plt_data["bpm3a-bpm3a"]["x"] == last_scan_data["samx"]["samx"].val
106
+ assert plt_data["bpm3a-bpm3a"]["y"] == last_scan_data["bpm3a"]["bpm3a"].val
107
+ assert plt_data["bpm4d-bpm4d"]["x"] == last_scan_data["samx"]["samx"].val
108
+ assert plt_data["bpm4d-bpm4d"]["y"] == last_scan_data["bpm4d"]["bpm4d"].val
109
+
110
+
111
+ def test_rpc_image(rpc_server, qtbot):
112
+ fig = BECFigure(rpc_server.gui_id)
113
+
114
+ im = fig.image("eiger")
115
+
116
+ client = rpc_server.client
117
+ dev = client.device_manager.devices
118
+ scans = client.scans
119
+
120
+ status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
121
+
122
+ # wait for scan to finish
123
+ while not status.status == "COMPLETED":
124
+ qtbot.wait(200)
125
+
126
+ last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
127
+ "data"
128
+ ].data
129
+ qtbot.wait(500)
130
+ last_image_plot = im.images[0].get_data()
131
+
132
+ # check plotted data
133
+ np.testing.assert_equal(last_image_device, last_image_plot)
134
+
135
+
136
+ def test_rpc_motor_map(rpc_server, qtbot):
137
+ fig = BECFigure(rpc_server.gui_id)
138
+ fig_server = rpc_server.fig
139
+
140
+ motor_map = fig.motor_map("samx", "samy")
141
+
142
+ client = rpc_server.client
143
+ dev = client.device_manager.devices
144
+ scans = client.scans
145
+
146
+ initial_pos_x = dev.samx.read()["samx"]["value"]
147
+ initial_pos_y = dev.samy.read()["samy"]["value"]
148
+
149
+ status = scans.mv(dev.samx, 1, dev.samy, 2, relative=True)
150
+
151
+ # wait for scan to finish
152
+ while not status.status == "COMPLETED":
153
+ qtbot.wait(200)
154
+ final_pos_x = dev.samx.read()["samx"]["value"]
155
+ final_pos_y = dev.samy.read()["samy"]["value"]
156
+
157
+ # check plotted data
158
+ motor_map_data = motor_map.get_data()
159
+
160
+ np.testing.assert_equal(
161
+ [motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
162
+ )
163
+ np.testing.assert_equal(
164
+ [motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
165
+ )
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
4
+
5
+
6
+ def find_deepest_value(d: dict):
7
+ """
8
+ Recursively find the deepest value in a dictionary
9
+ Args:
10
+ d(dict): Dictionary to search
11
+
12
+ Returns:
13
+ The deepest value in the dictionary.
14
+ """
15
+ if isinstance(d, dict):
16
+ if d:
17
+ return find_deepest_value(next(iter(d.values())))
18
+ return d
19
+
20
+
21
+ def test_rpc_register_list_connections(rpc_server, rpc_register, qtbot):
22
+ fig = BECFigure(rpc_server.gui_id)
23
+ fig_server = rpc_server.fig
24
+
25
+ plt = fig.plot("samx", "bpm4i")
26
+ im = fig.image("eiger")
27
+ motor_map = fig.motor_map("samx", "samy")
28
+ plt_z = fig.add_plot("samx", "samy", "bpm4i")
29
+
30
+ all_connections = rpc_register.list_all_connections()
31
+
32
+ # Construct dict of all rpc items manually
33
+ all_subwidgets_expected = dict(fig_server.widgets)
34
+ curve_1D = find_deepest_value(fig_server.widgets[plt.rpc_id]._curves_data)
35
+ curve_2D = find_deepest_value(fig_server.widgets[plt_z.rpc_id]._curves_data)
36
+ curves_expected = {curve_1D.rpc_id: curve_1D, curve_2D.rpc_id: curve_2D}
37
+ fig_expected = {fig.rpc_id: fig_server}
38
+ image_item_expected = {
39
+ fig_server.widgets[im.rpc_id].images[0].rpc_id: fig_server.widgets[im.rpc_id].images[0]
40
+ }
41
+
42
+ all_connections_expected = {
43
+ **all_subwidgets_expected,
44
+ **curves_expected,
45
+ **fig_expected,
46
+ **image_item_expected,
47
+ }
48
+
49
+ assert len(all_connections) == 8
50
+ assert all_connections == all_connections_expected
@@ -1,8 +1,15 @@
1
1
  import pytest
2
2
 
3
+ from bec_widgets.cli.rpc_register import RPCRegister
3
4
  from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
4
5
 
5
6
 
7
+ @pytest.fixture(autouse=True)
8
+ def rpc_register():
9
+ yield RPCRegister()
10
+ RPCRegister.reset_singleton()
11
+
12
+
6
13
  @pytest.fixture(autouse=True)
7
14
  def bec_dispatcher(threads_check):
8
15
  bec_dispatcher = bec_dispatcher_module.BECDispatcher()
@@ -0,0 +1,52 @@
1
+ from bec_widgets.cli.rpc_register import RPCRegister
2
+
3
+
4
+ class FakeObject:
5
+ def __init__(self, gui_id):
6
+ self.gui_id = gui_id
7
+
8
+
9
+ def test_add_connection(rpc_register):
10
+ obj1 = FakeObject("id1")
11
+ obj2 = FakeObject("id2")
12
+
13
+ rpc_register.add_rpc(obj1)
14
+ rpc_register.add_rpc(obj2)
15
+
16
+ all_connections = rpc_register.list_all_connections()
17
+
18
+ assert len(all_connections) == 2
19
+ assert all_connections["id1"] == obj1
20
+ assert all_connections["id2"] == obj2
21
+
22
+
23
+ def test_remove_connection(rpc_register):
24
+
25
+ obj1 = FakeObject("id1")
26
+ obj2 = FakeObject("id2")
27
+
28
+ rpc_register.add_rpc(obj1)
29
+ rpc_register.add_rpc(obj2)
30
+
31
+ rpc_register.remove_rpc(obj1)
32
+
33
+ all_connections = rpc_register.list_all_connections()
34
+
35
+ assert len(all_connections) == 1
36
+ assert all_connections["id2"] == obj2
37
+
38
+
39
+ def test_reset_singleton(rpc_register):
40
+ obj1 = FakeObject("id1")
41
+ obj2 = FakeObject("id2")
42
+
43
+ rpc_register.add_rpc(obj1)
44
+ rpc_register.add_rpc(obj2)
45
+
46
+ rpc_register.reset_singleton()
47
+ rpc_register = RPCRegister()
48
+
49
+ all_connections = rpc_register.list_all_connections()
50
+
51
+ assert len(all_connections) == 0
52
+ assert all_connections == {}