lima2.client 2.2.2__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.
- {lima2_client-2.2.2/src/lima2_client.egg-info → lima2_client-3.0.0}/PKG-INFO +5 -3
- {lima2_client-2.2.2 → lima2_client-3.0.0}/pyproject.toml +4 -2
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/draw_state_diagram.py +1 -2
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/state_machine.py +7 -5
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/client.py +31 -6
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/detector.py +196 -36
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/dense_frame.py +1 -2
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/smx_sparse_frame.py +1 -2
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/pipeline.py +296 -117
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/pipelines/__init__.py +3 -3
- lima2_client-3.0.0/src/lima2/client/pipelines/failing.py +51 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/pipelines/legacy.py +4 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/pipelines/smx.py +53 -6
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/pipelines/xpcs.py +20 -6
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/progress_counter.py +20 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/shell.py +1 -1
- lima2_client-3.0.0/src/lima2/client/state_machine.py +251 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/topology.py +21 -16
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/utils.py +23 -3
- lima2_client-3.0.0/src/lima2/master_file/generate.py +803 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0/src/lima2_client.egg-info}/PKG-INFO +5 -3
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2_client.egg-info/SOURCES.txt +2 -13
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2_client.egg-info/requires.txt +3 -1
- lima2_client-2.2.2/src/lima2/client/state_machine.py +0 -331
- lima2_client-2.2.2/src/lima2/master_file/generate.py +0 -247
- lima2_client-2.2.2/tests/test_client.py +0 -296
- lima2_client-2.2.2/tests/test_convert.py +0 -39
- lima2_client-2.2.2/tests/test_dense_frame.py +0 -155
- lima2_client-2.2.2/tests/test_detector.py +0 -142
- lima2_client-2.2.2/tests/test_pipeline.py +0 -688
- lima2_client-2.2.2/tests/test_pipelines.py +0 -20
- lima2_client-2.2.2/tests/test_progress_counter.py +0 -40
- lima2_client-2.2.2/tests/test_sparse_frame.py +0 -109
- lima2_client-2.2.2/tests/test_state_machine.py +0 -224
- lima2_client-2.2.2/tests/test_timer.py +0 -25
- lima2_client-2.2.2/tests/test_topology.py +0 -150
- lima2_client-2.2.2/tests/test_utils.py +0 -62
- {lima2_client-2.2.2 → lima2_client-3.0.0}/LICENSE +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/README.md +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/setup.cfg +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/__init__.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/__init__.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/async_github_pull_request.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/async_turnstile.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/boolean_field.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/github_pull_request.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/examples/turnstile.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/finite_state_machine/exceptions.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/__init__.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/convert.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/__init__.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/exception.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/sparse_frame.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/devencoded/structured_array.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/client/py.typed +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/server/__init__.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/server/control.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/server/receiver.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2/server/timer.py +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2_client.egg-info/dependency_links.txt +0 -0
- {lima2_client-2.2.2 → lima2_client-3.0.0}/src/lima2_client.egg-info/entry_points.txt +0 -0
- {lima2_client-2.2.2 → 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:
|
|
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.
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: pytango
|
|
@@ -24,7 +24,7 @@ Requires-Dist: gevent
|
|
|
24
24
|
Requires-Dist: jsonschema
|
|
25
25
|
Requires-Dist: jsonschema-default
|
|
26
26
|
Requires-Dist: beartype
|
|
27
|
-
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: numpy
|
|
28
28
|
Requires-Dist: pyyaml
|
|
29
29
|
Requires-Dist: typing-extensions
|
|
30
30
|
Requires-Dist: h5py
|
|
@@ -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.
|
|
10
|
+
requires-python = ">= 3.10"
|
|
11
11
|
dynamic = ["version"]
|
|
12
12
|
authors = [{ name = "BCU (ESRF)" }]
|
|
13
13
|
description = "Python client library for Lima2 detector"
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
|
24
24
|
"jsonschema",
|
|
25
25
|
"jsonschema-default",
|
|
26
26
|
"beartype",
|
|
27
|
-
"numpy
|
|
27
|
+
"numpy",
|
|
28
28
|
"pyyaml",
|
|
29
29
|
"typing-extensions",
|
|
30
30
|
"h5py",
|
|
@@ -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:
|
|
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
|
|
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:
|
|
21
|
-
target:
|
|
20
|
+
source: list | bool | int | str
|
|
21
|
+
target: bool | int | str
|
|
22
22
|
conditions: list
|
|
23
|
-
on_error:
|
|
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.
|
|
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:
|
|
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."""
|
|
@@ -299,7 +324,7 @@ class Client:
|
|
|
299
324
|
|
|
300
325
|
cleared = []
|
|
301
326
|
for uuid in receiver.pipelines:
|
|
302
|
-
if uuid != str(current)
|
|
327
|
+
if uuid != str(current):
|
|
303
328
|
logger.debug(
|
|
304
329
|
f"Erasing pipeline {uuid} from recv {receiver.dev_name()}"
|
|
305
330
|
)
|
|
@@ -314,7 +339,7 @@ class Client:
|
|
|
314
339
|
try:
|
|
315
340
|
cleared = cleared.union(future.result())
|
|
316
341
|
except Exception:
|
|
317
|
-
logger.
|
|
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:
|
|
85
|
+
self.__params_schema: dict | None = None
|
|
82
86
|
self.__fsm = fsm.StateMachine(self.__ctrl, self.__recvs)
|
|
83
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
442
|
+
parallel_exec(update_ctrl, update_recv)
|
|
425
443
|
|
|
426
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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:
|
|
30
|
+
idx: int | None
|
|
32
31
|
"""Index of the frame where 0 is the first frame"""
|
|
33
32
|
|
|
34
33
|
shape: tuple[int, ...]
|