bec-ipython-client 3.37.0__tar.gz → 3.88.0__tar.gz

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.

Potentially problematic release.


This version of bec-ipython-client might be problematic. Click here for more details.

Files changed (44) hide show
  1. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/.gitignore +3 -0
  2. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/PKG-INFO +2 -2
  3. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_startup.py +5 -2
  4. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/device_progress.py +11 -6
  5. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/ipython_live_updates.py +47 -19
  6. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/live_table.py +80 -35
  7. bec_ipython_client-3.88.0/bec_ipython_client/callbacks/move_device.py +218 -0
  8. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/utils.py +5 -23
  9. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/main.py +85 -8
  10. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/signals.py +9 -3
  11. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/demo.py +2 -1
  12. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/pyproject.toml +2 -2
  13. bec_ipython_client-3.88.0/tests/client_tests/conftest.py +19 -0
  14. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_bec_client.py +36 -1
  15. bec_ipython_client-3.88.0/tests/client_tests/test_ipython_live_updates.py +350 -0
  16. bec_ipython_client-3.88.0/tests/client_tests/test_live_table.py +423 -0
  17. bec_ipython_client-3.88.0/tests/client_tests/test_move_callback.py +223 -0
  18. bec_ipython_client-3.88.0/tests/end-2-end/_ensure_requirements_container.py +21 -0
  19. bec_ipython_client-3.88.0/tests/end-2-end/test_procedures_e2e.py +134 -0
  20. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_e2e.py +35 -13
  21. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_lib_e2e.py +36 -32
  22. bec_ipython_client-3.37.0/bec_ipython_client/callbacks/move_device.py +0 -156
  23. bec_ipython_client-3.37.0/tests/client_tests/test_ipython_live_updates.py +0 -159
  24. bec_ipython_client-3.37.0/tests/client_tests/test_live_table.py +0 -322
  25. bec_ipython_client-3.37.0/tests/client_tests/test_move_callback.py +0 -181
  26. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/__init__.py +0 -0
  27. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/beamline_mixin.py +0 -0
  28. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_magics.py +0 -0
  29. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/__init__.py +0 -0
  30. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
  31. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
  32. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
  33. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
  34. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
  35. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
  36. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
  37. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/__init__.py +0 -0
  38. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
  39. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/prettytable.py +0 -0
  40. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/progressbar.py +0 -0
  41. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_beamline_mixins.py +0 -0
  42. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_device_progress.py +0 -0
  43. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_pretty_table.py +0 -0
  44. {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/conftest.py +0 -0
@@ -9,6 +9,9 @@
9
9
  **/*.egg*
10
10
  **/*.env
11
11
 
12
+ # bec_widgets saved profiles
13
+ widgets_settings/profiles/*
14
+
12
15
  # recovery_config files
13
16
  recovery_config_*
14
17
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_ipython_client
3
- Version: 3.37.0
3
+ Version: 3.88.0
4
4
  Summary: BEC IPython client
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Topic :: Scientific/Engineering
10
- Requires-Python: >=3.10
10
+ Requires-Python: >=3.11
11
11
  Requires-Dist: bec-lib~=3.0
12
12
  Requires-Dist: ipython~=8.22
13
13
  Requires-Dist: numpy<3.0,>=1.24
@@ -32,9 +32,12 @@ except (BECAuthenticationError, KeyboardInterrupt) as exc:
32
32
  except Exception:
33
33
  sys.excepthook(*sys.exc_info())
34
34
  else:
35
- if bec.started and not _main_dict["args"].nogui and BECGuiClient is not None:
35
+ if bec.started and BECGuiClient is not None:
36
36
  gui = bec.gui = BECGuiClient()
37
- gui.start()
37
+ if _main_dict["args"].gui_id:
38
+ gui.connect_to_gui_server(_main_dict["args"].gui_id)
39
+ if not _main_dict["args"].nogui:
40
+ gui.show()
38
41
 
39
42
  _available_plugins = plugin_helper.get_ipython_client_startup_plugins(state="post")
40
43
  if _available_plugins:
@@ -14,11 +14,16 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
14
14
 
15
15
  REPORT_TYPE = "device_progress"
16
16
 
17
- def _run_update(self, device_names: str):
17
+ def core(self):
18
+ """core function to run the live updates for the table"""
19
+ self._wait_for_report_instructions()
20
+ self._run_update(self.report_instruction[self.REPORT_TYPE])
21
+
22
+ def _run_update(self, device_names: list[str]):
18
23
  """Run the update loop for the progress bar.
19
24
 
20
25
  Args:
21
- device_names (str): The name of the device to monitor.
26
+ device_names (list[str]): The name of the device to monitor.
22
27
  """
23
28
  with ScanProgressBar(
24
29
  scan_number=self.scan_item.scan_number, clear_on_exit=False
@@ -29,7 +34,7 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
29
34
  self._print_client_msgs_asap()
30
35
  self._print_client_msgs_all()
31
36
 
32
- def _update_progressbar(self, progressbar: ScanProgressBar, device_names: str) -> bool:
37
+ def _update_progressbar(self, progressbar: ScanProgressBar, device_names: list[str]) -> bool:
33
38
  """Update the progressbar based on the device status message
34
39
 
35
40
  Args:
@@ -41,17 +46,17 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
41
46
  self.check_alarms()
42
47
  status = self.bec.connector.get(MessageEndpoints.device_progress(device_names[0]))
43
48
  if not status:
44
- logger.debug("waiting for new data point")
49
+ logger.trace("waiting for new data point")
45
50
  time.sleep(0.1)
46
51
  return False
47
52
  if status.metadata.get("scan_id") != self.scan_item.scan_id:
48
- logger.debug("waiting for new data point")
53
+ logger.trace("waiting for new data point")
49
54
  time.sleep(0.1)
50
55
  return False
51
56
 
52
57
  point_id = status.content.get("value")
53
58
  if point_id is None:
54
- logger.debug("waiting for new data point")
59
+ logger.trace("waiting for new data point")
55
60
  time.sleep(0.1)
56
61
  return False
57
62
 
@@ -31,7 +31,7 @@ class IPythonLiveUpdates:
31
31
  self._interrupted_request = None
32
32
  self._active_callback = None
33
33
  self._processed_instructions = 0
34
- self._active_request = None
34
+ self._active_request: messages.ScanQueueMessage | None = None
35
35
  self._user_callback = None
36
36
  self._request_block_index = collections.defaultdict(lambda: 0)
37
37
  self._request_block_id = None
@@ -47,9 +47,11 @@ class IPythonLiveUpdates:
47
47
  Args:
48
48
  report_instructions (list): The list of report instructions.
49
49
  """
50
- scan_type = self._active_request.content["scan_type"]
50
+ if not self._active_request:
51
+ return
52
+ scan_type = self._active_request.scan_type
51
53
  if scan_type in ["open_scan_def", "close_scan_def"]:
52
- self._process_instruction({"scan_progress": 0})
54
+ self._process_instruction({"scan_progress": {"points": 0, "show_table": True}})
53
55
  return
54
56
  if scan_type == "close_scan_group":
55
57
  return
@@ -74,6 +76,9 @@ class IPythonLiveUpdates:
74
76
  scan_report_type = list(instr.keys())[0]
75
77
  scan_def_id = self.client.scans._scan_def_id
76
78
  interactive_scan = self.client.scans._interactive_scan
79
+ if self._active_request is None:
80
+ # Already checked in caller method. It is just for type checking purposes.
81
+ return
77
82
  if scan_def_id is None or interactive_scan:
78
83
  if scan_report_type == "readback":
79
84
  LiveUpdatesReadbackProgressbar(
@@ -126,17 +131,22 @@ class IPythonLiveUpdates:
126
131
  )
127
132
  self._active_callback.run()
128
133
 
129
- def _available_req_blocks(self, queue: QueueItem, request: messages.ScanQueueMessage):
134
+ def _available_req_blocks(
135
+ self, queue: QueueItem, request: messages.ScanQueueMessage
136
+ ) -> list[messages.RequestBlock]:
130
137
  """Get the available request blocks.
131
138
 
132
139
  Args:
133
140
  queue (QueueItem): The queue item.
134
141
  request (messages.ScanQueueMessage): The request message.
142
+
143
+ Returns:
144
+ list[messages.RequestBlock]: The list of available request blocks.
135
145
  """
136
146
  available_blocks = [
137
147
  req_block
138
148
  for req_block in queue.request_blocks
139
- if req_block["RID"] == request.metadata["RID"]
149
+ if req_block.RID == request.metadata["RID"]
140
150
  ]
141
151
  return available_blocks
142
152
 
@@ -151,11 +161,14 @@ class IPythonLiveUpdates:
151
161
  scan_request = ScanRequestMixin(self.client, request.metadata["RID"])
152
162
  scan_request.wait()
153
163
 
164
+ # After .wait, we can be sure that the queue item is available, so we can
165
+ assert scan_request.scan_queue_request is not None
166
+
154
167
  # get the corresponding queue item
155
- while not scan_request.request_storage.storage[-1].queue:
168
+ while not scan_request.scan_queue_request.queue:
156
169
  time.sleep(0.01)
157
170
 
158
- self._current_queue = queue = scan_request.request_storage.storage[-1].queue
171
+ self._current_queue = queue = scan_request.scan_queue_request.queue
159
172
  self._request_block_id = req_id = self._active_request.metadata.get("RID")
160
173
 
161
174
  while queue.status not in ["COMPLETED", "ABORTED", "HALTED"]:
@@ -164,7 +177,7 @@ class IPythonLiveUpdates:
164
177
 
165
178
  available_blocks = self._available_req_blocks(queue, request)
166
179
  req_block = available_blocks[self._request_block_index[req_id]]
167
- report_instructions = req_block.get("report_instructions", [])
180
+ report_instructions = req_block.report_instructions or []
168
181
  self._process_report_instructions(report_instructions)
169
182
 
170
183
  self._reset()
@@ -188,12 +201,19 @@ class IPythonLiveUpdates:
188
201
  self.client.queue.request_scan_halt()
189
202
 
190
203
  def _element_in_queue(self) -> bool:
191
- queue = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
192
- "info", []
193
- )
194
- if not queue:
204
+ if self.client.queue is None:
205
+ return False
206
+ if (csq := self.client.queue.queue_storage.current_scan_queue) is None:
195
207
  return False
196
- return self._current_queue.queue_id in queue[0].get("queue_id")
208
+ scan_queue_status = csq.get("primary")
209
+ if scan_queue_status is None:
210
+ return False
211
+ queue_info = scan_queue_status.info
212
+ if not queue_info:
213
+ return False
214
+ if self._current_queue is None:
215
+ return False
216
+ return self._current_queue.queue_id == queue_info[0].queue_id
197
217
 
198
218
  def _process_queue(
199
219
  self, queue: QueueItem, request: messages.ScanQueueMessage, req_id: str
@@ -213,9 +233,11 @@ class IPythonLiveUpdates:
213
233
  if not queue.request_blocks or not queue.status or queue.queue_position is None:
214
234
  return False
215
235
  if queue.status == "PENDING" and queue.queue_position > 0:
216
- status = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
217
- "status"
218
- )
236
+ primary_queue = self.client.queue.queue_storage.current_scan_queue.get("primary")
237
+
238
+ if primary_queue is None:
239
+ return False
240
+ status = primary_queue.status
219
241
  print(
220
242
  "Scan is enqueued and is waiting for execution. Current position in queue:"
221
243
  f" {queue.queue_position + 1}. Queue status: {status}.",
@@ -223,14 +245,16 @@ class IPythonLiveUpdates:
223
245
  flush=True,
224
246
  )
225
247
  available_blocks = self._available_req_blocks(queue, request)
248
+ if not available_blocks:
249
+ return False
226
250
  req_block = available_blocks[self._request_block_index[req_id]]
227
- if req_block["content"]["scan_type"] in [
251
+ if req_block.msg.scan_type in [
228
252
  "open_scan_def",
229
253
  "mv",
230
254
  ]: # TODO: make this more general for all scan types that don't have report instructions
231
255
  return True
232
256
 
233
- report_instructions = req_block["report_instructions"]
257
+ report_instructions = req_block.report_instructions or []
234
258
  if not report_instructions:
235
259
  return False
236
260
  self._process_report_instructions(report_instructions)
@@ -258,7 +282,11 @@ class IPythonLiveUpdates:
258
282
  self._current_queue = None
259
283
  self._user_callback = None
260
284
  self._processed_instructions = 0
261
- scan_closed = forced or (self._active_request.content["scan_type"] == "close_scan_def")
285
+ scan_closed = (
286
+ forced
287
+ or self._active_request is None
288
+ or (self._active_request.scan_type == "close_scan_def")
289
+ )
262
290
  self._active_request = None
263
291
 
264
292
  if self.client.scans._scan_def_id and not scan_closed:
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from collections.abc import Callable
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any, SupportsFloat
6
+
7
+ import numpy as np
6
8
 
7
9
  from bec_ipython_client.prettytable import PrettyTable
8
10
  from bec_ipython_client.progressbar import ScanProgressBar
@@ -54,7 +56,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
54
56
  super().__init__(
55
57
  bec, report_instruction=report_instruction, request=request, callbacks=callbacks
56
58
  )
57
- self.scan_queue_request = None
58
59
  self.scan_item = None
59
60
  self.dev_values = None
60
61
  self.point_data = None
@@ -66,16 +67,19 @@ class LiveUpdatesTable(LiveUpdatesBase):
66
67
  if print_table_data is not None
67
68
  else self.REPORT_TYPE == "scan_progress"
68
69
  )
70
+ self._devices_with_bad_precision = set()
69
71
 
70
72
  def wait_for_scan_to_start(self):
71
73
  """wait until the scan starts"""
72
74
  while True:
75
+ if not self.scan_item or not self.scan_item.queue:
76
+ raise RuntimeError("No scan item or scan queue available.")
73
77
  queue_pos = self.scan_item.queue.queue_position
74
78
  self.check_alarms()
75
79
  if self.scan_item.status == "closed":
76
80
  break
77
81
  if queue_pos is None:
78
- logger.debug(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
82
+ logger.trace(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
79
83
  continue
80
84
  if queue_pos == 0:
81
85
  break
@@ -157,7 +161,20 @@ class LiveUpdatesTable(LiveUpdatesBase):
157
161
  return header
158
162
 
159
163
  def update_scan_item(self, timeout: float = 15):
160
- """get the current scan item"""
164
+ """
165
+ Get the current scan item and update self.scan_item
166
+
167
+ Args:
168
+ timeout (float): timeout in seconds
169
+
170
+ Raises:
171
+ RuntimeError: if no scan queue request is available
172
+ TimeoutError: if no scan item is found before reaching the timeout
173
+
174
+ """
175
+ if not self.scan_queue_request:
176
+ raise RuntimeError("No scan queue request available.")
177
+
161
178
  start = time.time()
162
179
  while self.scan_queue_request.scan is None:
163
180
  self.check_alarms()
@@ -168,25 +185,38 @@ class LiveUpdatesTable(LiveUpdatesBase):
168
185
 
169
186
  def core(self):
170
187
  """core function to run the live updates for the table"""
188
+ self._wait_for_report_instructions()
189
+ show_table = self.report_instruction[self.REPORT_TYPE].get("show_table", True)
190
+ self._print_table_data = show_table
191
+ self._run_update(self.report_instruction[self.REPORT_TYPE]["points"])
192
+
193
+ def _wait_for_report_instructions(self):
194
+ """wait until the report instructions are available"""
195
+ if not self.scan_queue_request or not self.scan_item or not self.scan_item.queue:
196
+ logger.warning(
197
+ f"Cannot wait for report instructions. scan_queue_request: {self.scan_queue_request}, scan_item: {self.scan_item}, scan_item.queue: {getattr(self.scan_item, 'queue', None)}"
198
+ )
199
+ return
171
200
  req_ID = self.scan_queue_request.requestID
172
201
  while True:
173
202
  request_block = [
174
- req for req in self.scan_item.queue.request_blocks if req["RID"] == req_ID
203
+ req for req in self.scan_item.queue.request_blocks if req.RID == req_ID
175
204
  ][0]
176
- if not request_block["is_scan"]:
205
+ if not request_block.is_scan:
177
206
  break
178
- if request_block["report_instructions"]:
207
+ if request_block.report_instructions:
179
208
  break
180
209
  self.check_alarms()
181
210
 
182
- self._run_update(self.report_instruction[self.REPORT_TYPE])
183
-
184
211
  def _run_update(self, target_num_points: int):
185
212
  """run the update loop with the progress bar
186
213
 
187
214
  Args:
188
215
  target_num_points (int): number of points to be collected
189
216
  """
217
+ if not self.scan_item:
218
+ logger.warning("No scan item available for live updates.")
219
+ return
190
220
  with ScanProgressBar(
191
221
  scan_number=self.scan_item.scan_number, clear_on_exit=self._print_table_data
192
222
  ) as progressbar:
@@ -208,7 +238,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
208
238
  self.bec.callbacks.poll()
209
239
  self.scan_item.poll_callbacks()
210
240
  else:
211
- logger.debug("waiting for new data point")
241
+ logger.trace("waiting for new data point")
212
242
  time.sleep(0.1)
213
243
 
214
244
  if not self.scan_item.num_points:
@@ -218,12 +248,25 @@ class LiveUpdatesTable(LiveUpdatesBase):
218
248
  break
219
249
  if self.point_id > self.scan_item.num_points:
220
250
  raise RuntimeError("Received more points than expected.")
251
+
221
252
  if len(self.scan_item.live_data) == 0 and self.scan_item.status == "closed":
253
+ msg = self.scan_item.status_message
254
+ if not msg:
255
+ continue
256
+ if msg.readout_priority.get("monitored", []):
257
+ continue
258
+
222
259
  logger.warning(
223
260
  f"\n Scan {self.scan_item.scan_number} finished. No monitored devices enabled, please check your config."
224
261
  )
225
262
  break
226
263
 
264
+ def _warn_bad_precisions(self):
265
+ if self._devices_with_bad_precision != set():
266
+ for dev, prec in self._devices_with_bad_precision:
267
+ logger.warning(f"Device {dev} reported malformed precision of {prec}!")
268
+ self._devices_with_bad_precision = set()
269
+
227
270
  @property
228
271
  def _print_table_data(self) -> bool:
229
272
  """Checks if the table should be printed or not.
@@ -266,40 +309,41 @@ class LiveUpdatesTable(LiveUpdatesBase):
266
309
 
267
310
  if self.point_id % 100 == 0:
268
311
  print(self.table.get_header_lines())
269
- ind = 0
312
+
313
+ signals_precisions = []
270
314
  for dev in self.devices:
271
315
  if dev in self.bec.device_manager.devices:
272
316
  obj = self.bec.device_manager.devices[dev]
273
317
  for hint in obj._hints:
274
318
  signal = self.point_data.content["data"].get(obj.root.name, {}).get(hint)
275
- if signal is None or signal.get("value") is None:
276
- print_value = "N/A"
319
+ if signal is None:
320
+ signals_precisions.append((None, None))
277
321
  else:
278
- try:
279
- precision = getattr(obj, "precision")
280
- except AttributeError:
281
- precision = 2
282
- value = signal.get("value")
283
- if isinstance(value, (int, float)):
284
- print_value = f"{value:.{precision}f}"
285
- else:
286
- print_value = str(value)
287
- self.dev_values[ind] = print_value
288
- ind += 1
322
+ prec = getattr(obj, "precision", 2)
323
+ if not isinstance(prec, int):
324
+ self._devices_with_bad_precision.add((dev, prec))
325
+ prec = 2
326
+ signals_precisions.append((signal, prec))
289
327
  else:
290
- signal = self.point_data.content["data"].get(dev, {})
291
- value = signal.get("value")
292
- if value is not None:
293
- if isinstance(value, (int, float)):
294
- print_value = f"{value:.2f}"
295
- else:
296
- print_value = str(value)
297
- else:
298
- print_value = "N/A"
299
- self.dev_values[ind] = print_value
300
- ind += 1
328
+ signals_precisions.append((self.point_data.content["data"].get(dev, {}), 2))
329
+
330
+ for i, (signal, precision) in enumerate(signals_precisions):
331
+ self.dev_values[i] = self._format_value(signal, precision)
332
+
301
333
  print(self.table.get_row(str(self.point_id), *self.dev_values))
302
334
 
335
+ def _format_value(self, signal: dict | None, precision: int = 2):
336
+ if signal is None:
337
+ return "N/A"
338
+ val = signal.get("value")
339
+ if isinstance(val, SupportsFloat) and not isinstance(val, np.ndarray):
340
+ if precision < 0:
341
+ # This is to cover the special case when EPICS returns a negative precision.
342
+ # More info: https://epics.anl.gov/tech-talk/2004/msg00434.php
343
+ return f"{float(val):.{-precision}g}"
344
+ return f"{float(val):.{precision}f}"
345
+ return str(val)
346
+
303
347
  def close_table(self):
304
348
  """close the table and print the footer"""
305
349
  if not self.table:
@@ -310,6 +354,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
310
354
  f"Scan {self.scan_item.scan_number} finished. Scan ID {self.scan_item.scan_id}. Elapsed time: {elapsed_time:.2f} s"
311
355
  )
312
356
  )
357
+ self._warn_bad_precisions()
313
358
 
314
359
  def process_request(self):
315
360
  """process the request and start the core loop for live updates"""
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ import numpy as np
8
+
9
+ from bec_ipython_client.progressbar import DeviceProgressBar
10
+ from bec_lib import messages
11
+ from bec_lib.endpoints import MessageEndpoints
12
+ from bec_lib.redis_connector import MessageObject
13
+
14
+ from .utils import LiveUpdatesBase, check_alarms
15
+
16
+ if TYPE_CHECKING:
17
+ from bec_lib.client import BECClient
18
+ from bec_lib.devicemanager import DeviceManagerBase
19
+
20
+
21
+ class ReadbackDataHandler:
22
+ """Helper class to get the current device values and request-done messages."""
23
+
24
+ def __init__(
25
+ self, device_manager: DeviceManagerBase, devices: list[str], request_id: str
26
+ ) -> None:
27
+ """Helper class to get the current device values and request-done messages.
28
+
29
+ Args:
30
+ device_manager (DeviceManagerBase): device manager
31
+ devices (list): list of devices to monitor
32
+ request_id (str): request ID
33
+ """
34
+ self.device_manager = device_manager
35
+ self.devices = devices
36
+ self.connector = device_manager.connector
37
+ self.request_id = request_id
38
+ self._devices_received = {dev: False for dev in devices}
39
+ self.data: dict[str, messages.DeviceMessage] = {}
40
+ self._devices_done_state: dict[str, tuple[bool, bool]] = {
41
+ dev: (False, False) for dev in devices
42
+ }
43
+ self.requests_done = threading.Event()
44
+ self._register_callbacks()
45
+
46
+ def _register_callbacks(self):
47
+ """register callbacks for device readback messages."""
48
+ for dev in self.devices:
49
+ self.connector.register(
50
+ MessageEndpoints.device_readback(dev), cb=self.on_readback, parent=self, device=dev
51
+ )
52
+ self.connector.register(
53
+ MessageEndpoints.device_req_status(self.request_id),
54
+ cb=self.on_req_status,
55
+ from_start=True,
56
+ parent=self,
57
+ )
58
+
59
+ def _unregister_callbacks(self):
60
+ """unregister callbacks for device readback messages."""
61
+ for dev in self.devices:
62
+ self.connector.unregister(MessageEndpoints.device_readback(dev), cb=self.on_readback)
63
+ self.connector.unregister(
64
+ MessageEndpoints.device_req_status(self.request_id), cb=self.on_req_status
65
+ )
66
+
67
+ @staticmethod
68
+ def on_req_status(
69
+ msg_obj: dict[str, messages.DeviceReqStatusMessage], parent: ReadbackDataHandler
70
+ ):
71
+ """Callback for device request status messages to track which devices are done.
72
+
73
+ Args:
74
+ msg_obj (dict[str, messages.DeviceReqStatusMessage]): message object or device request status message
75
+ parent (ReadbackDataHandler): parent instance
76
+ """
77
+ # pylint: disable=protected-access
78
+ msg = msg_obj["data"]
79
+ if msg.request_id != parent.request_id:
80
+ return
81
+ device = msg.device
82
+ parent._devices_done_state[device] = (True, msg.success)
83
+
84
+ if (
85
+ all(done for done, _ in parent._devices_done_state.values())
86
+ and not parent.requests_done.is_set()
87
+ ):
88
+ parent._on_request_done()
89
+
90
+ @staticmethod
91
+ def on_readback(msg_obj: MessageObject, parent: ReadbackDataHandler, device: str):
92
+ """Callback for updating device readback data.
93
+
94
+ Args:
95
+ msg_obj (MessageObject): message object
96
+ parent (ReadbackDataHandler): parent instance
97
+ device (str): device name
98
+ """
99
+ # pylint: disable=protected-access
100
+ msg: messages.DeviceMessage = cast(messages.DeviceMessage, msg_obj.value)
101
+ parent._devices_received[device] = True
102
+ parent.data[device] = msg
103
+
104
+ def _on_request_done(self):
105
+ """Callback for when all requests are done."""
106
+ self.requests_done.set()
107
+ self._unregister_callbacks()
108
+
109
+ def get_device_values(self) -> list:
110
+ """get the current device values
111
+
112
+ Returns:
113
+ list: list of device values
114
+ """
115
+ values = []
116
+ for dev in self.devices:
117
+ val = self.data.get(dev)
118
+ if val is None:
119
+ signal_data = self.device_manager.devices[dev].read(cached=True)
120
+ else:
121
+ signal_data = val.signals
122
+ # pylint: disable=protected-access
123
+ hints = self.device_manager.devices[dev]._hints
124
+ # if we have hints, use them to get the value, otherwise just use the first value
125
+ if hints:
126
+ values.append(signal_data.get(hints[0]).get("value"))
127
+ else:
128
+ values.append(signal_data.get(list(signal_data.keys())[0]).get("value"))
129
+ return values
130
+
131
+ def done(self) -> bool:
132
+ """check if all devices are done
133
+
134
+ Returns:
135
+ bool: True if all devices are done, False otherwise
136
+ """
137
+ return self.requests_done.is_set()
138
+
139
+ def device_states(self) -> dict[str, tuple[bool, bool]]:
140
+ """
141
+ Return the current device done states.
142
+
143
+ Returns:
144
+ dict: dictionary with device names as keys and tuples of (done, success) as values
145
+ """
146
+ return self._devices_done_state
147
+
148
+
149
+ class LiveUpdatesReadbackProgressbar(LiveUpdatesBase):
150
+ """Live feedback on motor movements using a progressbar.
151
+
152
+ Args:
153
+ bec (BECClient): BECClient instance
154
+ report_instruction (list, optional): report instruction for the scan. Defaults to None.
155
+ request (messages.ScanQueueMessage, optional): scan queue request message. Defaults to None.
156
+ callbacks (list[Callable], optional): list of callbacks to register. Defaults to None.
157
+
158
+ """
159
+
160
+ def __init__(
161
+ self,
162
+ bec: BECClient,
163
+ report_instruction: list | None = None,
164
+ request: messages.ScanQueueMessage | None = None,
165
+ callbacks: list[Callable] | None = None,
166
+ ) -> None:
167
+ super().__init__(
168
+ bec, report_instruction=report_instruction, request=request, callbacks=callbacks
169
+ )
170
+ if report_instruction:
171
+ self.devices = report_instruction["readback"]["devices"]
172
+ else:
173
+ self.devices = list(request.content["parameter"]["args"].keys())
174
+
175
+ def core(self):
176
+ """core function to monitor the device values and update the progressbar accordingly."""
177
+ request_id = self.request.metadata["RID"]
178
+ if self.report_instruction:
179
+ self.devices = self.report_instruction["readback"]["devices"]
180
+ request_id = self.report_instruction["readback"]["RID"]
181
+ data_source = ReadbackDataHandler(self.bec.device_manager, self.devices, request_id)
182
+ start_values = data_source.get_device_values()
183
+ self.wait_for_request_acceptance()
184
+
185
+ if self.report_instruction:
186
+ target_values = self.report_instruction["readback"]["end"]
187
+
188
+ start_instr = self.report_instruction["readback"].get("start")
189
+ if start_instr:
190
+ start_values = self.report_instruction["readback"]["start"]
191
+ else:
192
+ target_values = [
193
+ x for xs in self.request.content["parameter"]["args"].values() for x in xs
194
+ ]
195
+ if self.request.content["parameter"]["kwargs"].get("relative"):
196
+ target_values = np.asarray(target_values) + np.asarray(start_values)
197
+
198
+ with DeviceProgressBar(
199
+ self.devices, start_values=start_values, target_values=target_values
200
+ ) as progress:
201
+
202
+ while not progress.finished or not data_source.done():
203
+ check_alarms(self.bec)
204
+
205
+ values = data_source.get_device_values()
206
+ progress.update(values=values)
207
+ self._print_client_msgs_asap()
208
+
209
+ for dev, (done, success) in data_source.device_states().items():
210
+ if done and success:
211
+ progress.set_finished(dev)
212
+ # pylint: disable=protected-access
213
+ progress._progress.refresh()
214
+ self._print_client_msgs_all()
215
+
216
+ def run(self):
217
+ """run the progressbar."""
218
+ self.core()