lima2.client 2.2.1__tar.gz → 3.0.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.
Files changed (62) hide show
  1. {lima2_client-2.2.1/src/lima2_client.egg-info → lima2_client-3.0.0}/PKG-INFO +4 -2
  2. {lima2_client-2.2.1 → lima2_client-3.0.0}/pyproject.toml +3 -1
  3. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/draw_state_diagram.py +1 -2
  4. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/state_machine.py +7 -5
  5. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/client.py +30 -5
  6. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/detector.py +196 -36
  7. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/dense_frame.py +1 -2
  8. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/smx_sparse_frame.py +1 -2
  9. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/pipeline.py +296 -117
  10. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/pipelines/__init__.py +3 -3
  11. lima2_client-3.0.0/src/lima2/client/pipelines/failing.py +51 -0
  12. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/pipelines/legacy.py +4 -0
  13. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/pipelines/smx.py +53 -6
  14. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/pipelines/xpcs.py +20 -6
  15. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/progress_counter.py +20 -0
  16. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/shell.py +1 -1
  17. lima2_client-3.0.0/src/lima2/client/state_machine.py +251 -0
  18. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/topology.py +21 -16
  19. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/utils.py +23 -3
  20. lima2_client-3.0.0/src/lima2/master_file/generate.py +803 -0
  21. {lima2_client-2.2.1 → lima2_client-3.0.0/src/lima2_client.egg-info}/PKG-INFO +4 -2
  22. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2_client.egg-info/SOURCES.txt +2 -13
  23. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2_client.egg-info/requires.txt +2 -0
  24. lima2_client-2.2.1/src/lima2/client/state_machine.py +0 -331
  25. lima2_client-2.2.1/src/lima2/master_file/generate.py +0 -247
  26. lima2_client-2.2.1/tests/test_client.py +0 -280
  27. lima2_client-2.2.1/tests/test_convert.py +0 -39
  28. lima2_client-2.2.1/tests/test_dense_frame.py +0 -155
  29. lima2_client-2.2.1/tests/test_detector.py +0 -142
  30. lima2_client-2.2.1/tests/test_pipeline.py +0 -688
  31. lima2_client-2.2.1/tests/test_pipelines.py +0 -20
  32. lima2_client-2.2.1/tests/test_progress_counter.py +0 -40
  33. lima2_client-2.2.1/tests/test_sparse_frame.py +0 -109
  34. lima2_client-2.2.1/tests/test_state_machine.py +0 -224
  35. lima2_client-2.2.1/tests/test_timer.py +0 -25
  36. lima2_client-2.2.1/tests/test_topology.py +0 -150
  37. lima2_client-2.2.1/tests/test_utils.py +0 -62
  38. {lima2_client-2.2.1 → lima2_client-3.0.0}/LICENSE +0 -0
  39. {lima2_client-2.2.1 → lima2_client-3.0.0}/README.md +0 -0
  40. {lima2_client-2.2.1 → lima2_client-3.0.0}/setup.cfg +0 -0
  41. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/__init__.py +0 -0
  42. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/__init__.py +0 -0
  43. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/async_github_pull_request.py +0 -0
  44. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/async_turnstile.py +0 -0
  45. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/boolean_field.py +0 -0
  46. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/github_pull_request.py +0 -0
  47. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/examples/turnstile.py +0 -0
  48. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/finite_state_machine/exceptions.py +0 -0
  49. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/__init__.py +0 -0
  50. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/convert.py +0 -0
  51. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/__init__.py +0 -0
  52. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/exception.py +0 -0
  53. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/sparse_frame.py +0 -0
  54. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/devencoded/structured_array.py +0 -0
  55. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/client/py.typed +0 -0
  56. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/server/__init__.py +0 -0
  57. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/server/control.py +0 -0
  58. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/server/receiver.py +0 -0
  59. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2/server/timer.py +0 -0
  60. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2_client.egg-info/dependency_links.txt +0 -0
  61. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2_client.egg-info/entry_points.txt +0 -0
  62. {lima2_client-2.2.1 → lima2_client-3.0.0}/src/lima2_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lima2-client
3
- Version: 2.2.1
3
+ Version: 3.0.0
4
4
  Summary: Python client library for Lima2 detector
5
5
  Author: BCU (ESRF)
6
6
  License: Copyright (c) 2020-2024 Beamline Control Unit, ESRF
@@ -16,7 +16,7 @@ Project-URL: Documentation, https://limagroup.gitlab-pages.esrf.fr/lima2-client/
16
16
  Classifier: Intended Audience :: Science/Research
17
17
  Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Programming Language :: Python :: 3
19
- Requires-Python: >=3.9
19
+ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: pytango
@@ -33,8 +33,10 @@ Requires-Dist: black==22.3; extra == "dev"
33
33
  Requires-Dist: mypy; extra == "dev"
34
34
  Requires-Dist: types-pyyaml; extra == "dev"
35
35
  Requires-Dist: types-jsonschema; extra == "dev"
36
+ Requires-Dist: types-gevent; extra == "dev"
36
37
  Requires-Dist: flake8; extra == "dev"
37
38
  Requires-Dist: ipython; extra == "dev"
39
+ Requires-Dist: nosqltangodb; extra == "dev"
38
40
  Requires-Dist: pytango-db; extra == "dev"
39
41
  Requires-Dist: pytest; extra == "dev"
40
42
  Requires-Dist: pytest-forked; extra == "dev"
@@ -7,7 +7,7 @@ version_scheme = "guess-next-dev"
7
7
 
8
8
  [project]
9
9
  name = "lima2-client"
10
- requires-python = ">= 3.9"
10
+ requires-python = ">= 3.10"
11
11
  dynamic = ["version"]
12
12
  authors = [{ name = "BCU (ESRF)" }]
13
13
  description = "Python client library for Lima2 detector"
@@ -43,8 +43,10 @@ dev = [
43
43
  "mypy",
44
44
  "types-pyyaml",
45
45
  "types-jsonschema",
46
+ "types-gevent",
46
47
  "flake8",
47
48
  "ipython",
49
+ "nosqltangodb",
48
50
  "pytango-db",
49
51
  "pytest",
50
52
  "pytest-forked",
@@ -3,7 +3,6 @@ import importlib
3
3
  import inspect
4
4
  import os
5
5
  import sys
6
- from typing import List
7
6
 
8
7
  from finite_state_machine.state_machine import TransitionDetails
9
8
 
@@ -28,7 +27,7 @@ def generate_state_diagram_markdown(cls, initial_state):
28
27
  """
29
28
 
30
29
  class_fns = inspect.getmembers(cls, predicate=inspect.isfunction)
31
- state_transitions: List[TransitionDetails] = [
30
+ state_transitions: list[TransitionDetails] = [
32
31
  func._fsm for name, func in class_fns if hasattr(func, "_fsm")
33
32
  ]
34
33
 
@@ -2,7 +2,7 @@ import asyncio
2
2
  from enum import Enum
3
3
  import functools
4
4
  import types
5
- from typing import NamedTuple, Union
5
+ from typing import NamedTuple
6
6
 
7
7
  from .exceptions import ConditionsNotMet, InvalidStartState
8
8
 
@@ -17,10 +17,10 @@ class StateMachine:
17
17
 
18
18
  class TransitionDetails(NamedTuple):
19
19
  name: str
20
- source: Union[list, bool, int, str]
21
- target: Union[bool, int, str]
20
+ source: list | bool | int | str
21
+ target: bool | int | str
22
22
  conditions: list
23
- on_error: Union[bool, int, str]
23
+ on_error: bool | int | str
24
24
 
25
25
 
26
26
  class transition:
@@ -95,11 +95,13 @@ class transition:
95
95
  result = func(*args, **kwargs)
96
96
  state_machine.state = self.target
97
97
  return result
98
- except Exception:
98
+ except Exception as e:
99
99
  # TODO should we log this somewhere?
100
100
  # logger.error? maybe have an optional parameter to set this up
101
101
  # how to libraries log?
102
102
  state_machine.state = self.on_error
103
+ if self.reraise:
104
+ raise e
103
105
  return
104
106
 
105
107
  @functools.wraps(func)
@@ -20,12 +20,12 @@ import logging
20
20
  import os
21
21
  import traceback
22
22
  from concurrent import futures
23
+ from typing import Any
23
24
  from uuid import UUID
24
25
 
25
26
  import tango as tg
26
27
  import yaml
27
28
  from beartype import beartype
28
- from typing_extensions import Any, Optional
29
29
 
30
30
  from lima2.client import pipelines, progress_counter, state_machine, topology
31
31
  from lima2.client.detector import Detector
@@ -157,13 +157,33 @@ class Client:
157
157
  acq_params=acq_params,
158
158
  proc_params=proc_params,
159
159
  num_receivers=len(self.receivers),
160
+ topology_kind=self.topology_kind,
160
161
  )
161
162
 
162
163
  self.detector.prepare_acq(uuid, ctl, acq, proc)
163
164
 
165
+ def on_error(evt: tg.DataReadyEventData):
166
+ logger.error(
167
+ f"Pipeline {evt.device.dev_name()} failed.\n"
168
+ f"Reason: '{evt.device.last_error}'"
169
+ )
170
+ if str(self.current_pipeline.uuid) in evt.device.dev_name():
171
+ logger.info(
172
+ f"Stopping current acquisition ({self.current_pipeline.uuid})..."
173
+ )
174
+ self.detector.abort_acq()
175
+
176
+ self.current_pipeline.register_on_error(on_error)
177
+
164
178
  @docstring_from(Detector.start_acq)
165
179
  def start_acq(self) -> None:
166
- self.detector.start_acq()
180
+ pipeline = self.current_pipeline
181
+ pipeline.register_on_finished(pipeline.unregister_events)
182
+ try:
183
+ self.detector.start_acq()
184
+ except Exception:
185
+ pipeline.unregister_events()
186
+ raise
167
187
 
168
188
  @docstring_from(Detector.trigger)
169
189
  def trigger(self) -> None:
@@ -218,7 +238,7 @@ class Client:
218
238
  return []
219
239
  return [UUID(u) for u in {y for x in uuids for y in x}]
220
240
 
221
- def pipeline(self, uuid: Optional[UUID] = None) -> Pipeline:
241
+ def pipeline(self, uuid: UUID | None = None) -> Pipeline:
222
242
  """Get a specific pipeline by uuid. Return the current one by default (uuid=None)"""
223
243
  if uuid is None:
224
244
  return self.current_pipeline
@@ -280,9 +300,14 @@ class Client:
280
300
 
281
301
  Raises if the pipeline is absent from a receiver.
282
302
  """
303
+
304
+ # First unregister Tango events
305
+ pipeline = self.__pipelines.pop(uuid, None)
306
+ if pipeline is not None:
307
+ pipeline.unregister_events()
308
+
283
309
  for recv in self.receivers:
284
310
  recv.erasePipeline(str(uuid))
285
- self.__pipelines.pop(uuid, None)
286
311
 
287
312
  def clear_previous_pipelines(self) -> None:
288
313
  """Erase all pipelines except the current one."""
@@ -314,7 +339,7 @@ class Client:
314
339
  try:
315
340
  cleared = cleared.union(future.result())
316
341
  except Exception:
317
- logger.warning(
342
+ logger.info(
318
343
  f"Exception while clearing pipelines from {receiver.dev_name()}: "
319
344
  f"{traceback.format_exc()}"
320
345
  )
@@ -18,14 +18,18 @@ Typical usage example:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import contextlib
21
22
  import json
22
23
  import logging
23
24
  import uuid
25
+ from collections.abc import Iterator
26
+
27
+ import gevent
24
28
 
25
29
  import tango
26
30
  from beartype import beartype
31
+ from gevent.lock import BoundedSemaphore
27
32
  from jsonschema_default import create_from
28
- from typing_extensions import Optional
29
33
 
30
34
  from lima2.client import pipelines
31
35
  from lima2.client import state_machine as fsm
@@ -78,9 +82,12 @@ class Detector:
78
82
  f"Receiver MPI rank order does not match client order: {rank_order}"
79
83
  )
80
84
 
81
- self.__params_schema: Optional[dict] = None
85
+ self.__params_schema: dict | None = None
82
86
  self.__fsm = fsm.StateMachine(self.__ctrl, self.__recvs)
83
- self.__try_sync_hard()
87
+ self.__fsm_lock = BoundedSemaphore()
88
+
89
+ self._register_state_change_handlers()
90
+ self.sync_hard()
84
91
 
85
92
  if _logger.getEffectiveLevel() == logging.DEBUG:
86
93
  # Log transition of states
@@ -93,6 +100,14 @@ class Detector:
93
100
  d.set_green_mode(tango.GreenMode.Gevent)
94
101
  d.set_timeout_millis(timeout * 1000)
95
102
 
103
+ @contextlib.contextmanager
104
+ def _locked_fsm(self) -> Iterator[fsm.StateMachine]:
105
+ self.__fsm_lock.acquire()
106
+ try:
107
+ yield self.__fsm
108
+ finally:
109
+ self.__fsm_lock.release()
110
+
96
111
  @property
97
112
  def det_info(self) -> dict:
98
113
  """
@@ -313,18 +328,8 @@ class Detector:
313
328
  Group:
314
329
  State
315
330
  """
316
- return self.__fsm.state
317
-
318
- def __try_sync_hard(self):
319
- """
320
- Try to init the state from a lazy constructed device Proxy
321
- (to be used when the DS is not running when Detector is constructed)
322
- """
323
- try:
324
- self.ping()
325
- self.sync_hard()
326
- except tango.DevFailed:
327
- return False
331
+ with self._locked_fsm() as fsm:
332
+ return fsm.state
328
333
 
329
334
  def sync_hard(self):
330
335
  """
@@ -333,12 +338,13 @@ class Detector:
333
338
  Group:
334
339
  State
335
340
  """
336
- try:
337
- self.__fsm.sync()
338
- except Exception as e:
339
- _logger.warning(f"Unable to sync with server: {e}")
341
+ with self._locked_fsm() as fsm:
342
+ try:
343
+ fsm.sync()
344
+ except Exception as e:
345
+ _logger.warning(f"Unable to sync with server: {e}")
340
346
 
341
- return self.__fsm.state
347
+ return fsm.state
342
348
 
343
349
  @property
344
350
  def dev_states(self):
@@ -366,7 +372,8 @@ class Detector:
366
372
  Group:
367
373
  State
368
374
  """
369
- self.__fsm.register_transition_logger(logger)
375
+ with self._locked_fsm() as fsm:
376
+ fsm.register_transition_logger(logger)
370
377
 
371
378
  def unregister_transition_logger(self, logger):
372
379
  """
@@ -378,7 +385,8 @@ class Detector:
378
385
  Group:
379
386
  State
380
387
  """
381
- self.__fsm.unregister_transition_logger(logger)
388
+ with self._locked_fsm() as fsm:
389
+ fsm.unregister_transition_logger(logger)
382
390
 
383
391
  @beartype
384
392
  def prepare_acq(
@@ -400,12 +408,18 @@ class Detector:
400
408
  f"prepare_acq({uuid}, {ctrl_params}, {acq_params}, {proc_params})"
401
409
  )
402
410
 
403
- validate(
404
- instance=ctrl_params,
405
- schema=self.params_schema[self.ctrl.name()]["acq_params"],
406
- )
411
+ def parallel_exec(ctrl_fn, recv_fn):
412
+ tasks = [gevent.spawn(ctrl_fn, self.__ctrl)]
413
+ tasks += [gevent.spawn(recv_fn, i, r) for i, r in enumerate(self.__recvs)]
414
+ gevent.joinall(tasks, raise_error=True)
407
415
 
408
- for i, recv in enumerate(self.__recvs):
416
+ def validate_ctrl(ctrl):
417
+ validate(
418
+ instance=ctrl_params,
419
+ schema=self.params_schema[ctrl.name()]["acq_params"],
420
+ )
421
+
422
+ def validate_recv(i, recv):
409
423
  validate(
410
424
  instance=acq_params[i],
411
425
  schema=self.params_schema[recv.name()]["acq_params"],
@@ -414,16 +428,22 @@ class Detector:
414
428
  pipeline_class = pipelines.get_class(proc_params[i]["class_name"])
415
429
  validate(instance=proc_params[i], schema=pipeline_class.params_schema)
416
430
 
431
+ parallel_exec(validate_ctrl, validate_recv)
432
+
417
433
  # Update parameters on the device servers
418
- # TODO: Rename acq_params to ctrl_params
419
- self.__ctrl.acq_params = json.dumps(ctrl_params)
420
- for i, recv in enumerate(self.__recvs):
434
+ def update_ctrl(ctrl):
435
+ # TODO: Rename acq_params to ctrl_params
436
+ ctrl.acq_params = json.dumps(ctrl_params)
437
+
438
+ def update_recv(i, recv):
421
439
  recv.acq_params = json.dumps(acq_params[i])
422
440
  recv.proc_params = json.dumps(proc_params[i])
423
441
 
424
- res = self.__fsm.prepare(str(uuid))
442
+ parallel_exec(update_ctrl, update_recv)
425
443
 
426
- return res.get() # Makes prepare_acq() synchronous
444
+ # Raises and reverts to IDLE on exception
445
+ with self._locked_fsm() as fsm:
446
+ fsm.prepare(str(uuid))
427
447
 
428
448
  def start_acq(self):
429
449
  """Start acquisition.
@@ -431,7 +451,11 @@ class Detector:
431
451
  Group:
432
452
  Control
433
453
  """
434
- self.__fsm.start()
454
+
455
+ self._register_on_end_acq_handlers()
456
+
457
+ with self._locked_fsm() as fsm:
458
+ fsm.start()
435
459
 
436
460
  def trigger(self):
437
461
  """Software trigger.
@@ -439,7 +463,11 @@ class Detector:
439
463
  Group:
440
464
  Control
441
465
  """
442
- self.__fsm.trigger()
466
+ with self._locked_fsm() as fsm:
467
+ if fsm.state == State.FAULT:
468
+ _logger.warning("Got trigger() while in FAULT state -> ignoring")
469
+ else:
470
+ fsm.trigger()
443
471
 
444
472
  def stop_acq(self):
445
473
  """
@@ -448,7 +476,9 @@ class Detector:
448
476
  Group:
449
477
  Control
450
478
  """
451
- self.__fsm.stop()
479
+ with self._locked_fsm() as fsm:
480
+ fsm.stop()
481
+ fsm.close()
452
482
 
453
483
  def reset_acq(self):
454
484
  """
@@ -457,7 +487,18 @@ class Detector:
457
487
  Group:
458
488
  Control
459
489
  """
460
- self.__fsm.reset_acq()
490
+ with self._locked_fsm() as fsm:
491
+ fsm.reset_acq()
492
+
493
+ def abort_acq(self):
494
+ """
495
+ Abort current acquisition (call Stop on running devices) and go to FAULT state.
496
+
497
+ Group:
498
+ Control
499
+ """
500
+ with self._locked_fsm() as fsm:
501
+ fsm.abort()
461
502
 
462
503
  @property
463
504
  def ctrl_params(self):
@@ -532,3 +573,122 @@ class Detector:
532
573
  # yield self.get_processing(self, uuid)
533
574
  # finally:
534
575
  # self.erase_processing(uuid)
576
+
577
+ def _register_state_change_handlers(self):
578
+ """Register a "change of acq_state" callback on each device.
579
+
580
+ These handlers are responsible for handling receivers entering FAULT state
581
+ (e.g. when the pipeline throws an exception), and for setting the global state
582
+ to DISCONNECTED if we receive errors on the tango event channel.
583
+ """
584
+
585
+ disconnected_devices = set()
586
+
587
+ def on_acq_state_change(evt: tango.EventData):
588
+ """Handler for state changes of each individual device"""
589
+ nonlocal disconnected_devices
590
+
591
+ # Handle disconnect
592
+ if evt.err:
593
+ if evt.device not in disconnected_devices:
594
+ _logger.error(
595
+ f"Lost connection to device {evt.device.dev_name()}:\n"
596
+ + "\n".join([f"- {err.desc}" for err in evt.errors])
597
+ + "\n"
598
+ )
599
+ disconnected_devices.add(evt.device)
600
+ with self._locked_fsm() as fsm:
601
+ fsm.disconnect()
602
+ return
603
+
604
+ # Handle FAULT
605
+ AcqState = type(evt.device.acq_state)
606
+ acq_state = AcqState(evt.attr_value.value)
607
+ _logger.debug(f"{evt.device.dev_name()} changed state to {acq_state.name}")
608
+
609
+ if acq_state == AcqState.fault:
610
+ _logger.error(
611
+ f"Device {evt.device.dev_name()} in FAULT state.\n"
612
+ f"Reason: '{evt.device.last_error}'\n"
613
+ )
614
+ if self.state not in [State.FAULT, State.UNKNOWN]:
615
+ _logger.error("Aborting acquisition.")
616
+ with self._locked_fsm() as fsm:
617
+ fsm.abort()
618
+
619
+ # Handle reconnect
620
+ if evt.device in disconnected_devices:
621
+ _logger.info(f"{evt.device.dev_name()} seems back up")
622
+ disconnected_devices.remove(evt.device)
623
+
624
+ if self.state == State.DISCONNECTED:
625
+ _logger.info("Trying to sync...")
626
+ with self._locked_fsm() as fsm:
627
+ try:
628
+ fsm.sync()
629
+ disconnected_devices = set()
630
+ print("Lima2 reconnected.")
631
+ except Exception as e:
632
+ _logger.warning(f"Sync failed: {e}")
633
+ finally:
634
+ _logger.info(f"Current state: {fsm.state}")
635
+
636
+ for dev in self.devs:
637
+ dev.subscribe_event(
638
+ "acq_state",
639
+ tango.EventType.CHANGE_EVENT,
640
+ on_acq_state_change,
641
+ )
642
+
643
+ def _register_on_end_acq_handlers(self):
644
+ """Register an "end of acquisition" callback on each receiver.
645
+
646
+ This handler callback is responsible for calling Close on the control device when
647
+ all receivers are finished transferring frames.
648
+ """
649
+ nb_frames_xferreds = []
650
+ event_ids = {}
651
+
652
+ def on_end_acq(evt):
653
+ nonlocal nb_frames_xferreds
654
+
655
+ # Filter spurious error events (if the device is restarted)
656
+ if evt.err:
657
+ _logger.error(
658
+ f"Error on {evt.device.dev_name()} 'nb_frames_xferred' event:\n"
659
+ + "\n".join([f"- {err.desc}" for err in evt.errors])
660
+ + "\n"
661
+ )
662
+ for r, e in event_ids.items():
663
+ r.unsubscribe_event(e)
664
+ return
665
+
666
+ nb_frames_xferreds.append(evt)
667
+
668
+ if len(nb_frames_xferreds) == len(self.recvs):
669
+ for r, e in event_ids.items():
670
+ r.unsubscribe_event(e)
671
+
672
+ _logger.debug(f"on_end_acq while {self.state}")
673
+ with self._locked_fsm() as fsm:
674
+ if fsm.state == State.RUNNING:
675
+ fsm.close()
676
+ elif fsm.state == State.FAULT:
677
+ # Acquisition aborted -> do nothing
678
+ pass
679
+ else:
680
+ # Unexpected
681
+ _logger.info(f"Acquisition ended while in state {fsm.state}")
682
+
683
+ nb_frames_xferreds = []
684
+
685
+ # On server side, data_ready_event is pushed by each receiver
686
+ # at the end of the acquisition loop.
687
+ event_ids = {
688
+ r: r.subscribe_event(
689
+ "nb_frames_xferred",
690
+ tango.EventType.DATA_READY_EVENT,
691
+ on_end_acq,
692
+ )
693
+ for r in self.recvs
694
+ }
@@ -11,7 +11,6 @@ import struct
11
11
  from dataclasses import dataclass
12
12
 
13
13
  import numpy
14
- from typing_extensions import Union
15
14
 
16
15
  from .exception import DevEncodedFormatNotSupported
17
16
 
@@ -19,7 +18,7 @@ DATA_HEADER_FORMAT = "<IHHIIHHHHHHHHIIIIIIQ"
19
18
  DATA_MAGIC = struct.unpack(">I", b"DTAY")[0]
20
19
  DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER_FORMAT)
21
20
 
22
- EncodedFrame = Union[bytes, tuple[str, bytes]]
21
+ EncodedFrame = bytes | tuple[str, bytes]
23
22
 
24
23
 
25
24
  class IMAGE_MODES(enum.Enum):
@@ -8,7 +8,6 @@
8
8
 
9
9
  import struct
10
10
  import numpy as np
11
- import typing
12
11
  from dataclasses import dataclass
13
12
 
14
13
  from .exception import DevEncodedFormatNotSupported
@@ -28,7 +27,7 @@ class SmxSparseFrame:
28
27
  background_avg: np.ndarray
29
28
  background_std: np.ndarray
30
29
 
31
- idx: typing.Optional[int]
30
+ idx: int | None
32
31
  """Index of the frame where 0 is the first frame"""
33
32
 
34
33
  shape: tuple[int, ...]