bec-widgets 0.53.2__py3-none-any.whl → 0.54.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.
Files changed (67) hide show
  1. CHANGELOG.md +24 -25
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +13 -13
  4. bec_widgets/cli/client_utils.py +0 -4
  5. bec_widgets/cli/generate_cli.py +7 -5
  6. bec_widgets/cli/server.py +5 -7
  7. bec_widgets/examples/jupyter_console/jupyter_console_window.py +7 -3
  8. bec_widgets/examples/motor_movement/motor_control_compilations.py +17 -16
  9. bec_widgets/widgets/__init__.py +0 -10
  10. bec_widgets/widgets/figure/figure.py +40 -23
  11. bec_widgets/widgets/figure/plots/__init__.py +0 -0
  12. bec_widgets/widgets/figure/plots/image/__init__.py +0 -0
  13. bec_widgets/widgets/{plots → figure/plots/image}/image.py +6 -416
  14. bec_widgets/widgets/figure/plots/image/image_item.py +277 -0
  15. bec_widgets/widgets/figure/plots/image/image_processor.py +152 -0
  16. bec_widgets/widgets/figure/plots/motor_map/__init__.py +0 -0
  17. bec_widgets/widgets/{plots → figure/plots/motor_map}/motor_map.py +2 -2
  18. bec_widgets/widgets/figure/plots/waveform/__init__.py +0 -0
  19. bec_widgets/widgets/{plots → figure/plots/waveform}/waveform.py +9 -222
  20. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +227 -0
  21. bec_widgets/widgets/motor_control/__init__.py +0 -7
  22. bec_widgets/widgets/motor_control/motor_control.py +2 -948
  23. bec_widgets/widgets/motor_control/motor_table/__init__.py +0 -0
  24. bec_widgets/widgets/motor_control/motor_table/motor_table.py +483 -0
  25. bec_widgets/widgets/motor_control/movement_absolute/__init__.py +0 -0
  26. bec_widgets/widgets/motor_control/movement_absolute/movement_absolute.py +157 -0
  27. bec_widgets/widgets/motor_control/movement_relative/__init__.py +0 -0
  28. bec_widgets/widgets/motor_control/movement_relative/movement_relative.py +227 -0
  29. bec_widgets/widgets/motor_control/selection/__init__.py +0 -0
  30. bec_widgets/widgets/motor_control/selection/selection.py +110 -0
  31. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/METADATA +1 -1
  32. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/RECORD +51 -52
  33. docs/requirements.txt +1 -0
  34. pyproject.toml +1 -1
  35. tests/end-2-end/test_bec_dock_rpc_e2e.py +1 -1
  36. tests/end-2-end/test_bec_figure_rpc_e2e.py +4 -4
  37. tests/end-2-end/test_rpc_register_e2e.py +1 -1
  38. tests/unit_tests/test_bec_dock.py +1 -1
  39. tests/unit_tests/test_bec_figure.py +6 -4
  40. tests/unit_tests/test_bec_motor_map.py +2 -3
  41. tests/unit_tests/test_motor_control.py +6 -5
  42. tests/unit_tests/test_waveform1d.py +13 -1
  43. bec_widgets/validation/__init__.py +0 -2
  44. bec_widgets/validation/monitor_config_validator.py +0 -258
  45. bec_widgets/widgets/monitor/__init__.py +0 -1
  46. bec_widgets/widgets/monitor/config_dialog.py +0 -574
  47. bec_widgets/widgets/monitor/config_dialog.ui +0 -210
  48. bec_widgets/widgets/monitor/example_configs/config_device.yaml +0 -60
  49. bec_widgets/widgets/monitor/example_configs/config_scans.yaml +0 -92
  50. bec_widgets/widgets/monitor/monitor.py +0 -845
  51. bec_widgets/widgets/monitor/tab_template.ui +0 -180
  52. bec_widgets/widgets/motor_map/__init__.py +0 -1
  53. bec_widgets/widgets/motor_map/motor_map.py +0 -594
  54. bec_widgets/widgets/plots/__init__.py +0 -4
  55. tests/unit_tests/test_bec_monitor.py +0 -220
  56. tests/unit_tests/test_config_dialog.py +0 -178
  57. tests/unit_tests/test_motor_map.py +0 -171
  58. tests/unit_tests/test_validator_errors.py +0 -110
  59. /bec_widgets/{cli → assets}/bec_widgets_icon.png +0 -0
  60. /bec_widgets/{examples/jupyter_console → assets}/terminal_icon.png +0 -0
  61. /bec_widgets/widgets/{plots → figure/plots}/plot_base.py +0 -0
  62. /bec_widgets/widgets/motor_control/{motor_control_table.ui → motor_table/motor_table.ui} +0 -0
  63. /bec_widgets/widgets/motor_control/{motor_control_absolute.ui → movement_absolute/movement_absolute.ui} +0 -0
  64. /bec_widgets/widgets/motor_control/{motor_control_relative.ui → movement_relative/movement_relative.ui} +0 -0
  65. /bec_widgets/widgets/motor_control/{motor_control_selection.ui → selection/selection.ui} +0 -0
  66. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/WHEEL +0 -0
  67. {bec_widgets-0.53.2.dist-info → bec_widgets-0.54.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,845 +0,0 @@
1
- # pylint: disable = no-name-in-module,missing-module-docstring
2
- import time
3
-
4
- import pyqtgraph as pg
5
- from bec_lib.endpoints import MessageEndpoints
6
- from pydantic import ValidationError
7
- from pyqtgraph import mkBrush, mkPen
8
- from qtpy import QtCore
9
- from qtpy.QtCore import Signal as pyqtSignal
10
- from qtpy.QtCore import Slot as pyqtSlot
11
- from qtpy.QtWidgets import QApplication, QMessageBox
12
-
13
- from bec_widgets.utils import Colors, Crosshair, yaml_dialog
14
- from bec_widgets.utils.bec_dispatcher import BECDispatcher
15
- from bec_widgets.validation import MonitorConfigValidator
16
- from bec_widgets.widgets.monitor.config_dialog import ConfigDialog
17
-
18
- # just for demonstration purposes if script run directly
19
- CONFIG_SCAN_MODE = {
20
- "plot_settings": {
21
- "background_color": "white",
22
- "num_columns": 3,
23
- "colormap": "plasma",
24
- "scan_types": True,
25
- },
26
- "plot_data": {
27
- "grid_scan": [
28
- {
29
- "plot_name": "Grid plot 1",
30
- "x_label": "Motor X",
31
- "y_label": "BPM",
32
- "sources": [
33
- {
34
- "type": "scan_segment",
35
- "signals": {
36
- "x": [{"name": "samx", "entry": "samx"}],
37
- "y": [{"name": "bpm4i"}],
38
- },
39
- }
40
- ],
41
- },
42
- {
43
- "plot_name": "Grid plot 2",
44
- "x_label": "Motor X",
45
- "y_label": "BPM",
46
- "sources": [
47
- {
48
- "type": "scan_segment",
49
- "signals": {
50
- "x": [{"name": "samx", "entry": "samx"}],
51
- "y": [{"name": "bpm4i"}],
52
- },
53
- }
54
- ],
55
- },
56
- {
57
- "plot_name": "Grid plot 3",
58
- "x_label": "Motor X",
59
- "y_label": "BPM",
60
- "sources": [
61
- {
62
- "type": "scan_segment",
63
- "signals": {"x": [{"name": "samy"}], "y": [{"name": "bpm4i"}]},
64
- }
65
- ],
66
- },
67
- {
68
- "plot_name": "Grid plot 4",
69
- "x_label": "Motor X",
70
- "y_label": "BPM",
71
- "sources": [
72
- {
73
- "type": "scan_segment",
74
- "signals": {
75
- "x": [{"name": "samy", "entry": "samy"}],
76
- "y": [{"name": "bpm4i"}],
77
- },
78
- }
79
- ],
80
- },
81
- ],
82
- "line_scan": [
83
- {
84
- "plot_name": "BPM plots vs samx",
85
- "x_label": "Motor X",
86
- "y_label": "Gauss",
87
- "sources": [
88
- {
89
- "type": "scan_segment",
90
- "signals": {
91
- "x": [{"name": "samx", "entry": "samx"}],
92
- "y": [{"name": "bpm4i"}],
93
- },
94
- }
95
- ],
96
- },
97
- {
98
- "plot_name": "Gauss plots vs samx",
99
- "x_label": "Motor X",
100
- "y_label": "Gauss",
101
- "sources": [
102
- {
103
- "type": "scan_segment",
104
- "signals": {
105
- "x": [{"name": "samx", "entry": "samx"}],
106
- "y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
107
- },
108
- }
109
- ],
110
- },
111
- ],
112
- },
113
- }
114
-
115
-
116
- CONFIG_WRONG = {
117
- "plot_settings": {
118
- "background_color": "black",
119
- "num_columns": 2,
120
- "colormap": "plasma",
121
- "scan_types": False,
122
- },
123
- "plot_data": [
124
- {
125
- "plot_name": "BPM4i plots vs samx",
126
- "x_label": "Motor Y",
127
- "y_label": "bpm4i",
128
- "sources": [
129
- {
130
- "type": "non_existing_source",
131
- "signals": {
132
- "x": [{"name": "samy"}],
133
- "y": [{"name": "bpm4i", "entry": "bpm4i"}],
134
- },
135
- },
136
- {
137
- "type": "history",
138
- "scan_id": "<scan_id>",
139
- "signals": {
140
- "x": [{"name": "samy"}],
141
- "y": [{"name": "bpm4i", "entry": "bpm4i"}],
142
- },
143
- },
144
- ],
145
- },
146
- {
147
- "plot_name": "Gauss plots vs samx",
148
- "x_label": "Motor X",
149
- "y_label": "Gauss",
150
- "sources": [
151
- {
152
- "type": "scan_segment",
153
- "signals": {
154
- "x": [{"name": "samx", "entry": "non_sense_entry"}],
155
- "y": [
156
- {"name": "non_existing_name"},
157
- {"name": "samy", "entry": "non_existing_entry"},
158
- ],
159
- },
160
- }
161
- ],
162
- },
163
- {
164
- "plot_name": "Gauss plots vs samx",
165
- "x_label": "Motor X",
166
- "y_label": "Gauss",
167
- "sources": [
168
- {
169
- "signals": {
170
- "x": [{"name": "samx", "entry": "samx"}],
171
- "y": [{"name": "samx"}, {"name": "samy", "entry": "samx"}],
172
- }
173
- }
174
- ],
175
- },
176
- ],
177
- }
178
-
179
-
180
- CONFIG_SIMPLE = {
181
- "plot_settings": {
182
- "background_color": "black",
183
- "num_columns": 2,
184
- "colormap": "plasma",
185
- "scan_types": False,
186
- },
187
- "plot_data": [
188
- {
189
- "plot_name": "BPM4i plots vs samx",
190
- "x_label": "Motor X",
191
- "y_label": "bpm4i",
192
- "sources": [
193
- {
194
- "type": "scan_segment",
195
- "signals": {
196
- "x": [{"name": "samx"}],
197
- "y": [{"name": "bpm4i", "entry": "bpm4i"}],
198
- },
199
- },
200
- # {
201
- # "type": "history",
202
- # "signals": {
203
- # "x": [{"name": "samx"}],
204
- # "y": [{"name": "bpm4i", "entry": "bpm4i"}],
205
- # },
206
- # },
207
- # {
208
- # "type": "dap",
209
- # 'worker':'some_worker',
210
- # "signals": {
211
- # "x": [{"name": "samx"}],
212
- # "y": [{"name": "bpm4i", "entry": "bpm4i"}],
213
- # },
214
- # },
215
- ],
216
- },
217
- {
218
- "plot_name": "Gauss plots vs samx",
219
- "x_label": "Motor X",
220
- "y_label": "Gauss",
221
- "sources": [
222
- {
223
- "type": "scan_segment",
224
- "signals": {
225
- "x": [{"name": "samx", "entry": "samx"}],
226
- "y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
227
- },
228
- }
229
- ],
230
- },
231
- ],
232
- }
233
-
234
- CONFIG_REDIS = {
235
- "plot_settings": {
236
- "background_color": "white",
237
- "axis_width": 2,
238
- "num_columns": 5,
239
- "colormap": "plasma",
240
- "scan_types": False,
241
- },
242
- "plot_data": [
243
- {
244
- "plot_name": "BPM4i plots vs samx",
245
- "x_label": "Motor Y",
246
- "y_label": "bpm4i",
247
- "sources": [
248
- {
249
- "type": "scan_segment",
250
- "signals": {"x": [{"name": "samx"}], "y": [{"name": "gauss_bpm"}]},
251
- },
252
- {
253
- "type": "redis",
254
- "endpoint": "public/gui/data/6cd5ea3f-a9a9-4736-b4ed-74ab9edfb996",
255
- "update": "append",
256
- "signals": {"x": [{"name": "x_default_tag"}], "y": [{"name": "y_default_tag"}]},
257
- },
258
- ],
259
- }
260
- ],
261
- }
262
-
263
-
264
- class BECMonitor(pg.GraphicsLayoutWidget):
265
- update_signal = pyqtSignal()
266
-
267
- def __init__(
268
- self,
269
- parent=None,
270
- client=None,
271
- config: dict = None,
272
- enable_crosshair: bool = True,
273
- gui_id=None,
274
- skip_validation: bool = False,
275
- ):
276
- super().__init__(parent=parent)
277
-
278
- # Client and device manager from BEC
279
- self.plot_data = None
280
- bec_dispatcher = BECDispatcher()
281
- self.client = bec_dispatcher.client if client is None else client
282
- self.dev = self.client.device_manager.devices
283
- self.queue = self.client.queue
284
-
285
- self.validator = MonitorConfigValidator(self.dev)
286
- self.gui_id = gui_id
287
-
288
- if self.gui_id is None:
289
- self.gui_id = self.__class__.__name__ + str(time.time())
290
-
291
- # Connect slots dispatcher
292
- bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
293
- bec_dispatcher.connect_slot(self.on_config_update, MessageEndpoints.gui_config(self.gui_id))
294
- bec_dispatcher.connect_slot(
295
- self.on_instruction, MessageEndpoints.gui_instructions(self.gui_id)
296
- )
297
- bec_dispatcher.connect_slot(self.on_data_from_redis, MessageEndpoints.gui_data(self.gui_id))
298
-
299
- # Current configuration
300
- self.config = config
301
- self.skip_validation = skip_validation
302
-
303
- # Enable crosshair
304
- self.enable_crosshair = enable_crosshair
305
-
306
- # Displayed Data
307
- self.database = None
308
-
309
- self.crosshairs = None
310
- self.plots = None
311
- self.curves_data = None
312
- self.grid_coordinates = None
313
- self.scan_id = None
314
-
315
- # TODO make colors accessible to users
316
- self.user_colors = {} # key: (plot_name, y_name, y_entry), value: color
317
-
318
- # Connect the update signal to the update plot method
319
- self.proxy_update_plot = pg.SignalProxy(
320
- self.update_signal, rateLimit=25, slot=self.update_scan_segment_plot
321
- )
322
-
323
- # Init UI
324
- if self.config is None:
325
- print("No initial config found for BECDeviceMonitor")
326
- else:
327
- self.on_config_update(self.config)
328
-
329
- def _init_config(self):
330
- """
331
- Initializes or update the configuration settings for the PlotApp.
332
- """
333
-
334
- # Separate configs
335
- self.plot_settings = self.config.get("plot_settings", {})
336
- self.plot_data_config = self.config.get("plot_data", {})
337
- self.scan_types = self.plot_settings.get("scan_types", False)
338
-
339
- if self.scan_types is False: # Device tracking mode
340
- self.plot_data = self.plot_data_config # TODO logic has to be improved
341
- else: # without incoming data setup the first configuration to the first scan type sorted alphabetically by name
342
- self.plot_data = self.plot_data_config[min(list(self.plot_data_config.keys()))]
343
-
344
- # Initialize the database
345
- self.database = self._init_database(self.plot_data)
346
-
347
- # Initialize the UI
348
- self._init_ui(self.plot_settings["num_columns"])
349
-
350
- if self.scan_id is not None:
351
- self.replot_last_scan()
352
-
353
- def _init_database(self, plot_data_config: dict, source_type_to_init=None) -> dict:
354
- """
355
- Initializes or updates the database for the PlotApp.
356
- Args:
357
- plot_data_config(dict): Configuration settings for plots.
358
- source_type_to_init(str, optional): Specific source type to initialize. If None, initialize all.
359
- Returns:
360
- dict: Updated or new database dictionary.
361
- """
362
- database = {} if source_type_to_init is None else self.database.copy()
363
-
364
- for plot in plot_data_config:
365
- for source in plot["sources"]:
366
- source_type = source["type"]
367
- if source_type_to_init and source_type != source_type_to_init:
368
- continue # Skip if not the specified source type
369
-
370
- if source_type not in database:
371
- database[source_type] = {}
372
-
373
- for axis, signals in source["signals"].items():
374
- for signal in signals:
375
- name = signal["name"]
376
- entry = signal.get("entry", name)
377
- if name not in database[source_type]:
378
- database[source_type][name] = {}
379
- if entry not in database[source_type][name]:
380
- database[source_type][name][entry] = []
381
-
382
- return database
383
-
384
- def _init_ui(self, num_columns: int = 3) -> None:
385
- """
386
- Initialize the UI components, create plots and store their grid positions.
387
-
388
- Args:
389
- num_columns (int): Number of columns to wrap the layout.
390
-
391
- This method initializes a dictionary `self.plots` to store the plot objects
392
- along with their corresponding x and y signal names. It dynamically arranges
393
- the plots in a grid layout based on the given number of columns and dynamically
394
- stretches the last plots to fit the remaining space.
395
- """
396
- self.clear()
397
- self.plots = {}
398
- self.grid_coordinates = []
399
-
400
- num_plots = len(self.plot_data)
401
-
402
- # Check if num_columns exceeds the number of plots
403
- if num_columns >= num_plots:
404
- num_columns = num_plots
405
- self.plot_settings["num_columns"] = num_columns # Update the settings
406
- print(
407
- "Warning: num_columns in the YAML file was greater than the number of plots."
408
- f" Resetting num_columns to number of plots:{num_columns}."
409
- )
410
- else:
411
- self.plot_settings["num_columns"] = num_columns # Update the settings
412
-
413
- num_rows = num_plots // num_columns
414
- last_row_cols = num_plots % num_columns
415
- remaining_space = num_columns - last_row_cols
416
-
417
- for i, plot_config in enumerate(self.plot_data):
418
- row, col = i // num_columns, i % num_columns
419
- colspan = 1
420
-
421
- if row == num_rows and remaining_space > 0:
422
- if last_row_cols == 1:
423
- colspan = num_columns
424
- else:
425
- colspan = remaining_space // last_row_cols + 1
426
- remaining_space -= colspan - 1
427
- last_row_cols -= 1
428
-
429
- plot_name = plot_config.get("plot_name", "")
430
-
431
- x_label = plot_config.get("x_label", "")
432
- y_label = plot_config.get("y_label", "")
433
-
434
- plot = self.addPlot(row=row, col=col, colspan=colspan, title=plot_name)
435
- plot.setLabel("bottom", x_label)
436
- plot.setLabel("left", y_label)
437
- plot.addLegend()
438
- self._set_plot_colors(plot, self.plot_settings)
439
-
440
- self.plots[plot_name] = plot
441
- self.grid_coordinates.append((row, col))
442
-
443
- # Initialize curves
444
- self.init_curves()
445
-
446
- def _set_plot_colors(self, plot: pg.PlotItem, plot_settings: dict) -> None:
447
- """
448
- Set the plot colors based on the plot config.
449
-
450
- Args:
451
- plot (pg.PlotItem): Plot object to set the colors.
452
- plot_settings (dict): Plot settings dictionary.
453
- """
454
- if plot_settings.get("show_grid", False):
455
- plot.showGrid(x=True, y=True, alpha=0.5)
456
- pen_width = plot_settings.get("axis_width")
457
- color = plot_settings.get("axis_color")
458
- if color is None:
459
- if plot_settings["background_color"].lower() == "black":
460
- color = "w"
461
- self.setBackground("k")
462
- elif plot_settings["background_color"].lower() == "white":
463
- color = "k"
464
- self.setBackground("w")
465
- else:
466
- raise ValueError(
467
- f"Invalid background color {plot_settings['background_color']}. Allowed values"
468
- " are 'white' or 'black'."
469
- )
470
- pen = pg.mkPen(color=color, width=pen_width)
471
- x_axis = plot.getAxis("bottom") # 'bottom' corresponds to the x-axis
472
- x_axis.setPen(pen)
473
- x_axis.setTextPen(pen)
474
- x_axis.setTickPen(pen)
475
-
476
- y_axis = plot.getAxis("left") # 'left' corresponds to the y-axis
477
- y_axis.setPen(pen)
478
- y_axis.setTextPen(pen)
479
- y_axis.setTickPen(pen)
480
-
481
- def init_curves(self) -> None:
482
- """
483
- Initialize curve data and properties for each plot and data source.
484
- """
485
- self.curves_data = {}
486
-
487
- for idx, plot_config in enumerate(self.plot_data):
488
- plot_name = plot_config.get("plot_name", "")
489
- plot = self.plots[plot_name]
490
- plot.clear()
491
-
492
- for source in plot_config["sources"]:
493
- source_type = source["type"]
494
- y_signals = source["signals"].get("y", [])
495
- colors_ys = Colors.golden_angle_color(
496
- colormap=self.plot_settings["colormap"], num=len(y_signals)
497
- )
498
-
499
- if source_type not in self.curves_data:
500
- self.curves_data[source_type] = {}
501
- if plot_name not in self.curves_data[source_type]:
502
- self.curves_data[source_type][plot_name] = []
503
-
504
- for i, (y_signal, color) in enumerate(zip(y_signals, colors_ys)):
505
- y_name = y_signal["name"]
506
- y_entry = y_signal.get("entry", y_name)
507
- curve_name = f"{y_name} ({y_entry})-{source_type[0].upper()}"
508
- curve_data = self.create_curve(curve_name, color)
509
- plot.addItem(curve_data)
510
- self.curves_data[source_type][plot_name].append((y_name, y_entry, curve_data))
511
-
512
- # Render static plot elements
513
- self.update_plot()
514
- # # Hook Crosshair #TODO enable later, currently not working
515
- if self.enable_crosshair is True:
516
- self.hook_crosshair()
517
-
518
- def create_curve(self, curve_name: str, color: str) -> pg.PlotDataItem:
519
- """
520
- Create
521
- Args:
522
- curve_name: Name of the curve
523
- color(str): Color of the curve
524
-
525
- Returns:
526
- pg.PlotDataItem: Assigned curve object
527
- """
528
- user_color = self.user_colors.get(curve_name, None)
529
- color_to_use = user_color if user_color else color
530
- pen_curve = mkPen(color=color_to_use, width=2, style=QtCore.Qt.DashLine)
531
- brush_curve = mkBrush(color=color_to_use)
532
-
533
- return pg.PlotDataItem(
534
- symbolSize=5,
535
- symbolBrush=brush_curve,
536
- pen=pen_curve,
537
- skipFiniteCheck=True,
538
- name=curve_name,
539
- )
540
-
541
- def hook_crosshair(self) -> None:
542
- """Hook the crosshair to all plots."""
543
- # TODO can be extended to hook crosshair signal for mouse move/clicked
544
- self.crosshairs = {}
545
- for plot_name, plot in self.plots.items():
546
- crosshair = Crosshair(plot, precision=3)
547
- self.crosshairs[plot_name] = crosshair
548
-
549
- def update_scan_segment_plot(self):
550
- """
551
- Update the plot with the latest scan segment data.
552
- """
553
- self.update_plot(source_type="scan_segment")
554
-
555
- def update_plot(self, source_type=None) -> None:
556
- """
557
- Update the plot data based on the stored data dictionary.
558
- Only updates data for the specified source_type if provided.
559
- """
560
- for src_type, plots in self.curves_data.items():
561
- if source_type and src_type != source_type:
562
- continue
563
-
564
- for plot_name, curve_list in plots.items():
565
- plot_config = next(
566
- (pc for pc in self.plot_data if pc.get("plot_name") == plot_name), None
567
- )
568
- if not plot_config:
569
- continue
570
-
571
- x_name, x_entry = self.extract_x_config(plot_config, src_type)
572
-
573
- for y_name, y_entry, curve in curve_list:
574
- data_x = self.database.get(src_type, {}).get(x_name, {}).get(x_entry, [])
575
- data_y = self.database.get(src_type, {}).get(y_name, {}).get(y_entry, [])
576
- curve.setData(data_x, data_y)
577
-
578
- def extract_x_config(self, plot_config: dict, source_type: str) -> tuple:
579
- """Extract the signal configurations for x and y axes from plot_config.
580
- Args:
581
- plot_config (dict): Plot configuration.
582
- Returns:
583
- tuple: Tuple containing the x name and x entry.
584
- """
585
- x_name, x_entry = None, None
586
-
587
- for source in plot_config["sources"]:
588
- if source["type"] == source_type and "x" in source["signals"]:
589
- x_signal = source["signals"]["x"][0]
590
- x_name = x_signal.get("name")
591
- x_entry = x_signal.get("entry", x_name)
592
- return x_name, x_entry
593
-
594
- def get_config(self):
595
- """Return the current configuration settings."""
596
- return self.config
597
-
598
- def show_config_dialog(self):
599
- """Show the configuration dialog."""
600
-
601
- dialog = ConfigDialog(
602
- client=self.client, default_config=self.config, skip_validation=self.skip_validation
603
- )
604
- dialog.config_updated.connect(self.on_config_update)
605
- dialog.show()
606
-
607
- def update_client(self, client) -> None:
608
- """Update the client and device manager from BEC.
609
- Args:
610
- client: BEC client
611
- """
612
- self.client = client
613
- self.dev = self.client.device_manager.devices
614
-
615
- def _close_all_plots(self):
616
- """Close all plots."""
617
- for plot in self.plots.values():
618
- plot.clear()
619
-
620
- @pyqtSlot(dict)
621
- def on_instruction(self, msg_content: dict) -> None:
622
- """
623
- Handle instructions sent to the GUI.
624
- Possible actions are:
625
- - clear: Clear the plots
626
- - close: Close the GUI
627
- - config_dialog: Open the configuration dialog
628
-
629
- Args:
630
- msg_content (dict): Message content with the instruction and parameters.
631
- """
632
- action = msg_content.get("action", None)
633
- parameters = msg_content.get("parameters", None)
634
-
635
- if action == "clear":
636
- self.flush()
637
- self._close_all_plots()
638
- elif action == "close":
639
- self.close()
640
- elif action == "config_dialog":
641
- self.show_config_dialog()
642
- else:
643
- print(f"Unknown instruction received: {msg_content}")
644
-
645
- @pyqtSlot(dict)
646
- def on_config_update(self, config: dict) -> None:
647
- """
648
- Validate and update the configuration settings for the PlotApp.
649
- Args:
650
- config(dict): Configuration settings
651
- """
652
- # convert config from BEC CLI to correct formatting
653
- config_tag = config.get("config", None)
654
- if config_tag is not None:
655
- config = config["config"]
656
-
657
- if self.skip_validation is True:
658
- self.config = config
659
- self._init_config()
660
- else:
661
- try:
662
- validated_config = self.validator.validate_monitor_config(config)
663
- self.config = validated_config.model_dump()
664
- self._init_config()
665
- except ValidationError as e:
666
- error_str = str(e)
667
- formatted_error_message = BECMonitor.format_validation_error(error_str)
668
-
669
- # Display the formatted error message in a popup
670
- QMessageBox.critical(self, "Configuration Error", formatted_error_message)
671
-
672
- @staticmethod
673
- def format_validation_error(error_str: str) -> str:
674
- """
675
- Format the validation error string to be displayed in a popup.
676
- Args:
677
- error_str(str): Error string from the validation error.
678
- """
679
- error_lines = error_str.split("\n")
680
- # The first line contains the number of errors.
681
- error_header = f"<p><b>{error_lines[0]}</b></p><hr>"
682
-
683
- formatted_error_message = error_header
684
- # Skip the first line as it's the header.
685
- error_details = error_lines[1:]
686
-
687
- # Iterate through pairs of lines (each error's two lines).
688
- for i in range(0, len(error_details), 2):
689
- location = error_details[i]
690
- message = error_details[i + 1] if i + 1 < len(error_details) else ""
691
-
692
- formatted_error_message += f"<p><b>{location}</b><br>{message}</p><hr>"
693
-
694
- return formatted_error_message
695
-
696
- def flush(self, flush_all=False, source_type_to_flush=None) -> None:
697
- """Update or reset the database to match the current configuration.
698
-
699
- Args:
700
- flush_all (bool): If True, reset the entire database.
701
- source_type_to_flush (str): Specific source type to reset. Ignored if flush_all is True.
702
- """
703
- if flush_all:
704
- self.database = self._init_database(self.plot_data)
705
- self.init_curves()
706
- else:
707
- if source_type_to_flush in self.database:
708
- # TODO maybe reinit the database from config again instead of cycle through names/entries
709
- # Reset only the specified source type
710
- for name in self.database[source_type_to_flush]:
711
- for entry in self.database[source_type_to_flush][name]:
712
- self.database[source_type_to_flush][name][entry] = []
713
- # Reset curves for the specified source type
714
- if source_type_to_flush in self.curves_data:
715
- self.init_curves()
716
-
717
- @pyqtSlot(dict, dict)
718
- def on_scan_segment(self, msg: dict, metadata: dict):
719
- """
720
- Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
721
-
722
- Args:
723
- msg (dict): Message received with scan data.
724
- metadata (dict): Metadata of the scan.
725
- """
726
- current_scan_id = msg.get("scan_id", None)
727
- if current_scan_id is None:
728
- return
729
-
730
- if current_scan_id != self.scan_id:
731
- if self.scan_types is False:
732
- self.plot_data = self.plot_data_config
733
- elif self.scan_types is True:
734
- current_name = metadata.get("scan_name")
735
- if current_name is None:
736
- raise ValueError(
737
- "Scan name not found in metadata. Please check the scan_name in the YAML"
738
- " config or in bec configuration."
739
- )
740
- self.plot_data = self.plot_data_config.get(current_name, None)
741
- if not self.plot_data:
742
- raise ValueError(
743
- f"Scan name {current_name} not found in the YAML config. Please check the scan_name in the "
744
- "YAML config or in bec configuration."
745
- )
746
-
747
- # Init UI
748
- self._init_ui(self.plot_settings["num_columns"])
749
-
750
- self.scan_id = current_scan_id
751
- self.scan_data = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
752
- if not self.scan_data:
753
- print(f"No data found for scan_id: {self.scan_id}") # TODO better error
754
- return
755
- self.flush(source_type_to_flush="scan_segment")
756
-
757
- self.scan_segment_update()
758
-
759
- self.update_signal.emit()
760
-
761
- def scan_segment_update(self):
762
- """
763
- Update the database with data from scan storage based on the provided scan_id.
764
- """
765
- scan_data = self.scan_data.data
766
- for device_name, device_entries in self.database.get("scan_segment", {}).items():
767
- for entry in device_entries.keys():
768
- dataset = scan_data[device_name][entry].val
769
- if dataset:
770
- self.database["scan_segment"][device_name][entry] = dataset
771
- else:
772
- print(f"No data found for {device_name} {entry}")
773
-
774
- def replot_last_scan(self):
775
- """
776
- Replot the last scan.
777
- """
778
- self.scan_segment_update()
779
- self.update_plot(source_type="scan_segment")
780
-
781
- @pyqtSlot(dict)
782
- def on_data_from_redis(self, msg) -> None:
783
- """
784
- Handle new data sent from redis.
785
- Args:
786
- msg (dict): Message received with data.
787
- """
788
-
789
- # wait until new config is loaded
790
- while "redis" not in self.database:
791
- time.sleep(0.1)
792
- self._init_database(
793
- self.plot_data, source_type_to_init="redis"
794
- ) # add database entry for redis dataset
795
-
796
- data = msg.get("data", {})
797
- x_data = data.get("x", {})
798
- y_data = data.get("y", {})
799
-
800
- # Update x data
801
- if x_data:
802
- x_tag = x_data.get("tag")
803
- self.database["redis"][x_tag][x_tag] = x_data["data"]
804
-
805
- # Update y data
806
- for y_tag, y_info in y_data.items():
807
- self.database["redis"][y_tag][y_tag] = y_info["data"]
808
-
809
- # Trigger plot update
810
- self.update_plot(source_type="redis")
811
- print(f"database after: {self.database}")
812
-
813
-
814
- if __name__ == "__main__": # pragma: no cover
815
- import argparse
816
- import json
817
- import sys
818
-
819
- parser = argparse.ArgumentParser()
820
- parser.add_argument("--config_file", help="Path to the config file.")
821
- parser.add_argument("--config", help="Path to the config file.")
822
- parser.add_argument("--id", help="GUI ID.")
823
- args = parser.parse_args()
824
-
825
- if args.config is not None:
826
- # Load config from file
827
- config = json.loads(args.config)
828
- elif args.config_file is not None:
829
- # Load config from file
830
- config = yaml_dialog.load_yaml(args.config_file)
831
- else:
832
- config = CONFIG_SIMPLE
833
-
834
- client = BECDispatcher().client
835
- client.start()
836
- app = QApplication(sys.argv)
837
- monitor = BECMonitor(config=config, gui_id=args.id, skip_validation=False)
838
- monitor.show()
839
- # just to test redis data
840
- # redis_data = {
841
- # "x": {"data": [1, 2, 3], "tag": "x_default_tag"},
842
- # "y": {"y_default_tag": {"data": [1, 2, 3]}},
843
- # }
844
- # monitor.on_data_from_redis({"data": redis_data})
845
- sys.exit(app.exec())