pymodaq_scripting 5.3.0a1__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.
- pymodaq_scripting-5.3.0a1/.gitignore +133 -0
- pymodaq_scripting-5.3.0a1/LICENSE +21 -0
- pymodaq_scripting-5.3.0a1/PKG-INFO +67 -0
- pymodaq_scripting-5.3.0a1/README.rst +11 -0
- pymodaq_scripting-5.3.0a1/pyproject.toml +74 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/__init__.py +1 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/devices.py +107 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/examples/__init__.py +0 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/examples/dashboard_script.py +27 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/examples/simple_script.py +35 -0
- pymodaq_scripting-5.3.0a1/src/pymodaq_scripting/utils.py +415 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Compiled python modules.
|
|
2
|
+
*.pyc
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
.idea/*
|
|
14
|
+
|
|
15
|
+
# Distribution / packaging
|
|
16
|
+
.Python
|
|
17
|
+
documentation/_*
|
|
18
|
+
build/
|
|
19
|
+
develop-eggs/
|
|
20
|
+
dist/
|
|
21
|
+
downloads/
|
|
22
|
+
eggs/
|
|
23
|
+
.eggs/
|
|
24
|
+
lib/
|
|
25
|
+
lib64/
|
|
26
|
+
parts/
|
|
27
|
+
sdist/
|
|
28
|
+
var/
|
|
29
|
+
wheels/
|
|
30
|
+
*.egg-info/
|
|
31
|
+
.installed.cfg
|
|
32
|
+
*.egg
|
|
33
|
+
MANIFEST
|
|
34
|
+
|
|
35
|
+
# PyInstaller
|
|
36
|
+
# Usually these files are written by a python script from a template
|
|
37
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
38
|
+
*.manifest
|
|
39
|
+
*.spec
|
|
40
|
+
|
|
41
|
+
# Installer logs
|
|
42
|
+
pip-log.txt
|
|
43
|
+
pip-delete-this-directory.txt
|
|
44
|
+
|
|
45
|
+
# Unit test / coverage reports
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
|
|
57
|
+
*.idea
|
|
58
|
+
|
|
59
|
+
# Translations
|
|
60
|
+
*.mo
|
|
61
|
+
*.pot
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Django stuff:
|
|
65
|
+
*.log
|
|
66
|
+
local_settings.py
|
|
67
|
+
db.sqlite3
|
|
68
|
+
|
|
69
|
+
# Flask stuff:
|
|
70
|
+
instance/
|
|
71
|
+
.webassets-cache
|
|
72
|
+
|
|
73
|
+
# Scrapy stuff:
|
|
74
|
+
.scrapy
|
|
75
|
+
|
|
76
|
+
# Sphinx documentation
|
|
77
|
+
docs/_build/
|
|
78
|
+
|
|
79
|
+
# PyBuilder
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# celery beat schedule file
|
|
89
|
+
celerybeat-schedule
|
|
90
|
+
|
|
91
|
+
# SageMath parsed files
|
|
92
|
+
*.sage.py
|
|
93
|
+
|
|
94
|
+
# Environments
|
|
95
|
+
.env
|
|
96
|
+
.venv
|
|
97
|
+
env/
|
|
98
|
+
venv/
|
|
99
|
+
ENV/
|
|
100
|
+
env.bak/
|
|
101
|
+
venv.bak/
|
|
102
|
+
|
|
103
|
+
# Spyder project settings
|
|
104
|
+
.spyderproject
|
|
105
|
+
.spyproject
|
|
106
|
+
|
|
107
|
+
# VS code project settings
|
|
108
|
+
.vscode/*
|
|
109
|
+
|
|
110
|
+
# Rope project settings
|
|
111
|
+
.ropeproject
|
|
112
|
+
|
|
113
|
+
# mkdocs documentation
|
|
114
|
+
/site
|
|
115
|
+
|
|
116
|
+
# mypy
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
.idea/workspace.xml
|
|
119
|
+
|
|
120
|
+
*.iml
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
.tox
|
|
124
|
+
*.h5
|
|
125
|
+
!data.h5
|
|
126
|
+
|
|
127
|
+
*yacctab.py
|
|
128
|
+
*lextab.py
|
|
129
|
+
|
|
130
|
+
.mu_repo
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
**/README.rst
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Sebastien Weber <sebastien.weber@cemes.fr>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pymodaq_scripting
|
|
3
|
+
Version: 5.3.0a1
|
|
4
|
+
Summary: Objects and utilities to use PyMoDAQ from a script
|
|
5
|
+
Project-URL: Homepage, http://pymodaq.cnrs.fr
|
|
6
|
+
Project-URL: Source, https://github.com/PyMoDAQ/pymodaq_scripting
|
|
7
|
+
Project-URL: Tracker, https://github.com/PyMoDAQ/pymodaq_scripting/issues
|
|
8
|
+
Author-email: Sébastien Weber <sebastien.weber@cemes.fr>
|
|
9
|
+
License: The MIT License (MIT)
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2021 Sebastien Weber <sebastien.weber@cemes.fr>
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in
|
|
21
|
+
all copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
29
|
+
THE SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
32
|
+
Classifier: Environment :: Other Environment
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Natural Language :: English
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
44
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: pyleco
|
|
47
|
+
Requires-Dist: pymodaq-data>=5.0.13
|
|
48
|
+
Requires-Dist: pymodaq-utils>=0.0.8
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
51
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest-xdist; extra == 'dev'
|
|
55
|
+
Description-Content-Type: text/x-rst
|
|
56
|
+
|
|
57
|
+
.. ############################################################
|
|
58
|
+
.. WARNING: This file is AUTO-GENERATED from README.rst.tpl.
|
|
59
|
+
.. ⚠️ DO NOT EDIT MANUALLY ⚠️
|
|
60
|
+
.. ############################################################
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
PyMoDAQ Scripting
|
|
64
|
+
#################
|
|
65
|
+
|
|
66
|
+
Set of Objects to use PyMoDAQ Dasboard control modules from an external script
|
|
67
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
.. ############################################################
|
|
2
|
+
.. WARNING: This file is AUTO-GENERATED from README.rst.tpl.
|
|
3
|
+
.. ⚠️ DO NOT EDIT MANUALLY ⚠️
|
|
4
|
+
.. ############################################################
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
PyMoDAQ Scripting
|
|
8
|
+
#################
|
|
9
|
+
|
|
10
|
+
Set of Objects to use PyMoDAQ Dasboard control modules from an external script
|
|
11
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.9.0", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pymodaq_scripting"
|
|
7
|
+
dynamic = [
|
|
8
|
+
"version",
|
|
9
|
+
]
|
|
10
|
+
description = "Objects and utilities to use PyMoDAQ from a script"
|
|
11
|
+
readme = "README.rst"
|
|
12
|
+
license = { file="LICENSE" }
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Sébastien Weber", email = "sebastien.weber@cemes.fr" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Environment :: Other Environment",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Natural Language :: English",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Human Machine Interfaces",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Software Development :: User Interfaces",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
dependencies = [
|
|
36
|
+
"pymodaq_utils>=0.0.8",
|
|
37
|
+
"pymodaq_data>=5.0.13",
|
|
38
|
+
"pyleco",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
|
|
43
|
+
dev = [
|
|
44
|
+
"hatch",
|
|
45
|
+
"flake8",
|
|
46
|
+
"pytest",
|
|
47
|
+
"pytest-cov",
|
|
48
|
+
"pytest-xdist",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "http://pymodaq.cnrs.fr"
|
|
54
|
+
Source = "https://github.com/PyMoDAQ/pymodaq_scripting"
|
|
55
|
+
Tracker = "https://github.com/PyMoDAQ/pymodaq_scripting/issues"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.version.raw-options]
|
|
58
|
+
search_parent_directories = true
|
|
59
|
+
fallback_version = "5.1.0"
|
|
60
|
+
git_describe_command = "python git_describe.py packages/pymodaq_scripting"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.version]
|
|
63
|
+
source = "vcs"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = [
|
|
68
|
+
"/src",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.run]
|
|
72
|
+
omit = [
|
|
73
|
+
"*/examples/*",
|
|
74
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .devices import Actuator, Detector, Dashboard
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from concurrent.futures import Future
|
|
2
|
+
from typing import Optional, TYPE_CHECKING, Union
|
|
3
|
+
from xml.etree.ElementTree import Element
|
|
4
|
+
from pymodaq_data.data import DataToExport
|
|
5
|
+
from pymodaq_scripting.utils import (
|
|
6
|
+
Device,
|
|
7
|
+
LECOActuatorWrapper,
|
|
8
|
+
LECODetectorWrapper,
|
|
9
|
+
LECODashboardWrapper,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pymodaq.utils.data import DataActuator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Actuator(Device[LECOActuatorWrapper]):
|
|
17
|
+
"""
|
|
18
|
+
Public interface to control an Actuator
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, device: str, **kwargs) -> None:
|
|
21
|
+
super().__init__(LECOActuatorWrapper(device, **kwargs))
|
|
22
|
+
|
|
23
|
+
def get_actuator_value(self) -> Future['DataActuator']:
|
|
24
|
+
return self._wrapper.get_actuator_value()
|
|
25
|
+
|
|
26
|
+
def move_home(self) -> Future['DataActuator']:
|
|
27
|
+
return self._wrapper.move_home()
|
|
28
|
+
|
|
29
|
+
def move_abs(self, value: Union['DataActuator', int, float, str]) -> Future['DataActuator']:
|
|
30
|
+
return self._wrapper.move_abs(value)
|
|
31
|
+
|
|
32
|
+
def move_rel(self, value: Union['DataActuator', int, float, str]) -> Future['DataActuator']:
|
|
33
|
+
return self._wrapper.move_rel(value)
|
|
34
|
+
|
|
35
|
+
def stop_move(self) -> Future['DataActuator']:
|
|
36
|
+
return self._wrapper.stop_move()
|
|
37
|
+
|
|
38
|
+
def get_settings(self) -> Future[Element]:
|
|
39
|
+
return self._wrapper.get_settings()
|
|
40
|
+
|
|
41
|
+
def set_settings(self, settings: Element) -> None:
|
|
42
|
+
self._wrapper.set_settings(settings)
|
|
43
|
+
|
|
44
|
+
class Detector(Device[LECODetectorWrapper]):
|
|
45
|
+
"""
|
|
46
|
+
Public interface to control a Detector
|
|
47
|
+
"""
|
|
48
|
+
def __init__(self, device: str, **kwargs) -> None:
|
|
49
|
+
super().__init__(LECODetectorWrapper(device, **kwargs))
|
|
50
|
+
|
|
51
|
+
def snap(self) -> Optional[Future[DataToExport]]:
|
|
52
|
+
return self._wrapper.snap()
|
|
53
|
+
|
|
54
|
+
def grab(self, keep=False) -> Optional[list[DataToExport]]:
|
|
55
|
+
return self._wrapper.grab(keep=keep)
|
|
56
|
+
|
|
57
|
+
def stop_grab(self) -> None:
|
|
58
|
+
return self._wrapper.stop_grab()
|
|
59
|
+
|
|
60
|
+
def get_settings(self) -> Future[Element]:
|
|
61
|
+
return self._wrapper.get_settings()
|
|
62
|
+
|
|
63
|
+
def set_settings(self, settings: Element) -> None:
|
|
64
|
+
self._wrapper.set_settings(settings)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Dashboard(Device[LECODashboardWrapper]):
|
|
68
|
+
"""
|
|
69
|
+
Public interface to control a Dashboard
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, **kwargs) -> None:
|
|
73
|
+
super().__init__(LECODashboardWrapper(**kwargs))
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def leco_name(self):
|
|
77
|
+
return self._wrapper.leco_name
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
return self._wrapper.name
|
|
82
|
+
|
|
83
|
+
def sign_out(self) -> None:
|
|
84
|
+
self._wrapper.sign_out()
|
|
85
|
+
|
|
86
|
+
def get_devices(self) -> Future[dict[str, list[str]]]:
|
|
87
|
+
return self._wrapper.get_devices()
|
|
88
|
+
|
|
89
|
+
def get_scripting_devices(self) -> dict[str, dict[str, Actuator | Detector]]:
|
|
90
|
+
devices = self._wrapper.get_devices().result()
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
'actuators': {name: Actuator(name) for name in devices['actuators']},
|
|
94
|
+
'detectors': {name: Detector(name) for name in devices['detectors']},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def get_states(self) -> Future[list[str]]:
|
|
98
|
+
return self._wrapper.get_states()
|
|
99
|
+
|
|
100
|
+
def apply_state(self, state: str) -> Future[bool]:
|
|
101
|
+
return self._wrapper.apply_state(state)
|
|
102
|
+
|
|
103
|
+
def get_experiments(self) -> Future[list[str]]:
|
|
104
|
+
return self._wrapper.get_experiments()
|
|
105
|
+
|
|
106
|
+
def apply_experiment(self, experiment: str) -> Future[bool]:
|
|
107
|
+
return self._wrapper.apply_experiment(experiment)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pymodaq_scripting import Detector, Actuator, Dashboard
|
|
2
|
+
|
|
3
|
+
# Once a dashboard is started:
|
|
4
|
+
dashboard = Dashboard()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
print(dashboard.get_experiments().result())
|
|
8
|
+
# >>> ['default']
|
|
9
|
+
# Could make it so that it returns a future that completes when it's finished loading
|
|
10
|
+
dashboard.apply_experiment('default')
|
|
11
|
+
|
|
12
|
+
print(dashboard.get_states().result())
|
|
13
|
+
# >>> ['default', 'my_custom_config']
|
|
14
|
+
|
|
15
|
+
# Same remark as dashboard.apply_preset
|
|
16
|
+
dashboard.apply_state('default')
|
|
17
|
+
|
|
18
|
+
print(dashboard.get_devices().result()) # returns the names
|
|
19
|
+
# >>> {'actuators': ['Theta', 'Temperature', 'Power', 'Xaxis'], 'detectors': ['Det2D', 'Det0D', 'Det1D']}
|
|
20
|
+
|
|
21
|
+
devices = dashboard.get_scripting_devices()
|
|
22
|
+
print(devices)
|
|
23
|
+
# >>> {'actuators': {'Theta': <Actuator object>, 'Temperature': <Actuator object>, 'Power': <Actuator object>, 'Xaxis': <Actuator object>}, 'detectors': {'Det2D': <Detector object>, 'Det0D': <Detector object>, 'Det1D': <Detector object>}}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
#Then each device can be used
|
|
27
|
+
devices['actuators']['Angle'].move_abs('90°')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pymodaq_data import Q_
|
|
4
|
+
from pymodaq_data.h5modules.backends import GroupType
|
|
5
|
+
|
|
6
|
+
from pymodaq_scripting import Detector, Actuator
|
|
7
|
+
from pymodaq_data.h5modules.data_saving import DataToExportEnlargeableSaver
|
|
8
|
+
|
|
9
|
+
theta = Actuator('Angle')
|
|
10
|
+
det0d = Detector('Det0D')
|
|
11
|
+
det1d = Detector('Det1D')
|
|
12
|
+
|
|
13
|
+
acquisitions = []
|
|
14
|
+
|
|
15
|
+
base = theta.get_actuator_value().result()
|
|
16
|
+
target = base + Q_('90°') # Or a DataActuator(data=90, units='°')
|
|
17
|
+
step = Q_('10°')
|
|
18
|
+
|
|
19
|
+
count = (target - base)/step
|
|
20
|
+
count = int(count.quantities[0].m[0]) + 1
|
|
21
|
+
|
|
22
|
+
filename = Path(r'C:\Data\2026\scripted_data1.h5')
|
|
23
|
+
|
|
24
|
+
with DataToExportEnlargeableSaver(filename, enl_axis_names=('count',), enl_axis_units=('',)) as saver:
|
|
25
|
+
|
|
26
|
+
group = saver.add_group('Script', group_type=GroupType.data ,where='/RawData/', title='scripted_data')
|
|
27
|
+
group0d = saver.add_det_group(where=group, title=det0d.name)
|
|
28
|
+
group1d = saver.add_det_group(where=group, title=det1d.name)
|
|
29
|
+
for idx in range(count):
|
|
30
|
+
value = theta.move_abs(base + idx * step).result() # taking directly the result makes it a blocking move
|
|
31
|
+
dte0d_future = det0d.snap() # async snap
|
|
32
|
+
dte1d_future = det1d.snap() # async snap
|
|
33
|
+
saver.add_data(group0d, dte0d_future.result(), (value.value(), ))
|
|
34
|
+
saver.add_data(group1d, dte1d_future.result(), (value.value(),))
|
|
35
|
+
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import Future, InvalidStateError
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import Optional, cast, Union, Generic, TypeVar, TYPE_CHECKING
|
|
8
|
+
from xml.etree.ElementTree import Element
|
|
9
|
+
|
|
10
|
+
from pyleco.directors.director import Director
|
|
11
|
+
from pyleco.utils.listener import Listener
|
|
12
|
+
from serializall import SerializableFactory, utils
|
|
13
|
+
|
|
14
|
+
from pymodaq_data.data import DataToExport
|
|
15
|
+
from pymodaq_data import Q_, DataRaw
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pymodaq.utils.data import DataActuator
|
|
19
|
+
|
|
20
|
+
sf = SerializableFactory()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compare_xml_trees(base: Element, modified: Element) -> list[tuple[list[str], bytes]]:
|
|
24
|
+
"""
|
|
25
|
+
Compare two XML trees and return changes.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
list of tuples: (path_list, changed_element_bytes)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
changes = []
|
|
32
|
+
|
|
33
|
+
def compare_elements(base_elem, modified_elem, path):
|
|
34
|
+
"""Recursively compare elements"""
|
|
35
|
+
# Compare text content (strip whitespace)
|
|
36
|
+
text1 = (base_elem.text or '').strip()
|
|
37
|
+
text2 = (modified_elem.text or '').strip()
|
|
38
|
+
|
|
39
|
+
if text1 != text2:
|
|
40
|
+
# Text changed - record this element
|
|
41
|
+
changed_bytes = ET.tostring(modified_elem)
|
|
42
|
+
changes.append((path.copy(), changed_bytes))
|
|
43
|
+
return # Don't recurse further into this branch
|
|
44
|
+
|
|
45
|
+
# Compare attributes (optional - if you want to detect attribute changes)
|
|
46
|
+
if base_elem.attrib != modified_elem.attrib:
|
|
47
|
+
changed_bytes = ET.tostring(modified_elem)
|
|
48
|
+
changes.append((path.copy(), changed_bytes))
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Compare children
|
|
52
|
+
children1 = list(base_elem)
|
|
53
|
+
children2 = list(modified_elem)
|
|
54
|
+
|
|
55
|
+
# Build tag-to-element mapping
|
|
56
|
+
children1_dict = {child.tag: child for child in children1}
|
|
57
|
+
children2_dict = {child.tag: child for child in children2}
|
|
58
|
+
|
|
59
|
+
# Recursively compare matching children
|
|
60
|
+
for tag in children1_dict.keys():
|
|
61
|
+
if tag in children2_dict:
|
|
62
|
+
compare_elements(children1_dict[tag], children2_dict[tag],
|
|
63
|
+
path + [tag])
|
|
64
|
+
|
|
65
|
+
# Start comparison with forced prefix: ['detector_settings', 'settings_client']
|
|
66
|
+
initial_path = [base.tag or modified.tag]
|
|
67
|
+
compare_elements(base, modified, initial_path)
|
|
68
|
+
|
|
69
|
+
return changes
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def value_to_data_actuator(value: Union['DataActuator', int, float, str]) -> Union['DataActuator', DataRaw]:
|
|
73
|
+
"""
|
|
74
|
+
Converts any kind of convertible value into a DataActuator
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
value: The value to convert
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
A DataActuator object containing the value
|
|
82
|
+
"""
|
|
83
|
+
if any(isinstance(value, t) for t in (int, float)):
|
|
84
|
+
return DataRaw(name='actuator', data=value)
|
|
85
|
+
if isinstance(value, str):
|
|
86
|
+
value = Q_(value)
|
|
87
|
+
if isinstance(value, Q_):
|
|
88
|
+
return DataRaw(name='actuator', data=value.magnitude, units=str(value.units))
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LECOBaseWrapper:
|
|
93
|
+
"""
|
|
94
|
+
Common LECO infrastructure: listener, communicator, director setup.
|
|
95
|
+
Shared by all LECO device and dashboard wrappers.
|
|
96
|
+
"""
|
|
97
|
+
def __init__(self, device_name: str, **kwargs) -> None:
|
|
98
|
+
self._device_name = device_name
|
|
99
|
+
|
|
100
|
+
self._listener = Listener(name=self.leco_name, timeout=None)
|
|
101
|
+
self._listener.start_listen()
|
|
102
|
+
|
|
103
|
+
self._communicator = self._listener.get_communicator()
|
|
104
|
+
self._director = Director(actor=self.name, communicator=self._communicator, **kwargs)
|
|
105
|
+
atexit.register(self._clean)
|
|
106
|
+
|
|
107
|
+
@cached_property
|
|
108
|
+
def leco_name(self) -> str:
|
|
109
|
+
return f'scripting_{self.name}'
|
|
110
|
+
|
|
111
|
+
@cached_property
|
|
112
|
+
def name(self) -> str:
|
|
113
|
+
return self._device_name
|
|
114
|
+
|
|
115
|
+
def _clean(self):
|
|
116
|
+
self.sign_out()
|
|
117
|
+
self._listener.close()
|
|
118
|
+
self._director.close()
|
|
119
|
+
self._communicator.close()
|
|
120
|
+
|
|
121
|
+
def sign_out(self):
|
|
122
|
+
self._director.ask_rpc('sign_out', actor='COORDINATOR')
|
|
123
|
+
|
|
124
|
+
def set_remote_name(self):
|
|
125
|
+
self._director.ask_rpc('set_remote_name', name=self.leco_name)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class LECODeviceWrapper(LECOBaseWrapper):
|
|
129
|
+
"""
|
|
130
|
+
LECO wrapper with settings management, shared by actuator and detector devices.
|
|
131
|
+
"""
|
|
132
|
+
def __init__(self, device: str, **kwargs) -> None:
|
|
133
|
+
self._base_settings: Optional[Element] = None
|
|
134
|
+
self._settings_future: Optional[Future[Element]] = None
|
|
135
|
+
|
|
136
|
+
super().__init__(device, **kwargs)
|
|
137
|
+
|
|
138
|
+
self._listener.register_rpc_method(self.set_director_settings)
|
|
139
|
+
self._listener.register_binary_rpc_method(self.set_director_info, accept_binary_input=True)
|
|
140
|
+
|
|
141
|
+
def get_settings(self) -> Future[Element]:
|
|
142
|
+
future = Future()
|
|
143
|
+
self._settings_future = future
|
|
144
|
+
|
|
145
|
+
self.set_remote_name()
|
|
146
|
+
self._director.ask_rpc("get_settings")
|
|
147
|
+
|
|
148
|
+
return future
|
|
149
|
+
|
|
150
|
+
def set_settings(self, settings: Element):
|
|
151
|
+
if self._base_settings is not None:
|
|
152
|
+
for (path, modified) in compare_xml_trees(self._base_settings, settings):
|
|
153
|
+
t_name, t_len = utils.str_len_to_bytes('ParameterWithPath')
|
|
154
|
+
data = t_len + t_name + sf.get_apply_serializer(path) + sf.get_apply_serializer(modified)
|
|
155
|
+
self._director.ask_rpc("set_info", parameter=None, additional_payload=[data])
|
|
156
|
+
|
|
157
|
+
def set_director_settings(self, settings: bytes):
|
|
158
|
+
# Important to have two creations so they can be compared
|
|
159
|
+
# for modifications later.
|
|
160
|
+
self._base_settings = ET.fromstring(settings)
|
|
161
|
+
try:
|
|
162
|
+
self._settings_future.set_result(ET.fromstring(settings))
|
|
163
|
+
self._settings_future = None
|
|
164
|
+
except (InvalidStateError, AttributeError):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def set_director_info(self, parameter=None, additional_payload=None) -> None:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LECOActuatorWrapper(LECODeviceWrapper):
|
|
173
|
+
"""
|
|
174
|
+
LECO wrapper for actuators: move commands and position callbacks.
|
|
175
|
+
"""
|
|
176
|
+
def __init__(self, device: str, **kwargs) -> None:
|
|
177
|
+
self._send_position_future: Optional[Future['DataActuator']] = None
|
|
178
|
+
self._move_done_future: Optional[Future['DataActuator']] = None
|
|
179
|
+
|
|
180
|
+
super().__init__(device, **kwargs)
|
|
181
|
+
|
|
182
|
+
self._listener.register_binary_rpc_method(self.send_position, accept_binary_input=True)
|
|
183
|
+
self._listener.register_binary_rpc_method(self.set_move_done, accept_binary_input=True)
|
|
184
|
+
|
|
185
|
+
def get_actuator_value(self) -> Future['DataActuator']:
|
|
186
|
+
future = Future()
|
|
187
|
+
self._send_position_future = future
|
|
188
|
+
|
|
189
|
+
self.set_remote_name()
|
|
190
|
+
self._director.ask_rpc('get_actuator_value')
|
|
191
|
+
|
|
192
|
+
return future
|
|
193
|
+
|
|
194
|
+
def move_home(self) -> Future['DataActuator']:
|
|
195
|
+
future = Future()
|
|
196
|
+
self._move_done_future = future
|
|
197
|
+
|
|
198
|
+
self.set_remote_name()
|
|
199
|
+
self._director.ask_rpc("move_home")
|
|
200
|
+
|
|
201
|
+
return future
|
|
202
|
+
|
|
203
|
+
def move_abs(self, value: Union['DataActuator', int, float, str]) -> Future['DataActuator']:
|
|
204
|
+
future = Future()
|
|
205
|
+
self._move_done_future = future
|
|
206
|
+
|
|
207
|
+
value = value_to_data_actuator(value)
|
|
208
|
+
serialize = SerializableFactory().get_apply_serializer
|
|
209
|
+
|
|
210
|
+
self.set_remote_name()
|
|
211
|
+
self._director.ask_rpc("move_abs", position=None, additional_payload=[serialize(value)])
|
|
212
|
+
|
|
213
|
+
return future
|
|
214
|
+
|
|
215
|
+
def move_rel(self, value: 'DataActuator') -> Future['DataActuator']:
|
|
216
|
+
future = Future()
|
|
217
|
+
self._move_done_future = future
|
|
218
|
+
|
|
219
|
+
value = value_to_data_actuator(value)
|
|
220
|
+
serialize = SerializableFactory().get_apply_serializer
|
|
221
|
+
|
|
222
|
+
self.set_remote_name()
|
|
223
|
+
self._director.ask_rpc("move_rel", position=None, additional_payload=[serialize(value)])
|
|
224
|
+
|
|
225
|
+
return future
|
|
226
|
+
|
|
227
|
+
def stop_move(self) -> Future['DataActuator']:
|
|
228
|
+
future = Future()
|
|
229
|
+
self._move_done_future = future
|
|
230
|
+
|
|
231
|
+
self.set_remote_name()
|
|
232
|
+
self._director.ask_rpc("stop_motion")
|
|
233
|
+
|
|
234
|
+
return future
|
|
235
|
+
|
|
236
|
+
def send_position(self, data=None, additional_payload=None):
|
|
237
|
+
value: Union['DataActuator', DataRaw] = cast(DataRaw, sf.get_apply_deserializer(additional_payload[0]))
|
|
238
|
+
try:
|
|
239
|
+
self._send_position_future.set_result(value)
|
|
240
|
+
self._send_position_future = None
|
|
241
|
+
except (InvalidStateError, AttributeError):
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def set_move_done(self, data=None, additional_payload=None):
|
|
245
|
+
value: Union['DataActuator', DataRaw] = cast(DataRaw, sf.get_apply_deserializer(additional_payload[0]))
|
|
246
|
+
try:
|
|
247
|
+
self._move_done_future.set_result(value)
|
|
248
|
+
self._move_done_future = None
|
|
249
|
+
except (InvalidStateError, AttributeError):
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class LECODetectorWrapper(LECODeviceWrapper):
|
|
254
|
+
"""
|
|
255
|
+
LECO wrapper for detectors: snap/grab commands and data callbacks.
|
|
256
|
+
"""
|
|
257
|
+
def __init__(self, device: str, **kwargs) -> None:
|
|
258
|
+
self._snap_data_future: Optional[Future[DataToExport]] = None
|
|
259
|
+
self._grab_data_list: Optional[list[DataToExport]] = None
|
|
260
|
+
self._is_grabbing = False
|
|
261
|
+
super().__init__(device, **kwargs)
|
|
262
|
+
self._listener.register_binary_rpc_method(self.set_data, accept_binary_input=True)
|
|
263
|
+
|
|
264
|
+
def snap(self) -> Optional[Future[DataToExport]]:
|
|
265
|
+
if not self._is_grabbing:
|
|
266
|
+
future = Future()
|
|
267
|
+
self._snap_data_future = future
|
|
268
|
+
self.set_remote_name()
|
|
269
|
+
self._director.ask_rpc("send_data_snap")
|
|
270
|
+
return future
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
def grab(self, keep=False) -> Optional[list[DataToExport]]:
|
|
274
|
+
was_grabbing = self._is_grabbing
|
|
275
|
+
if not was_grabbing:
|
|
276
|
+
self._grab_data_list = [] if keep else None
|
|
277
|
+
|
|
278
|
+
self.set_remote_name()
|
|
279
|
+
self._director.ask_rpc("send_data_grab")
|
|
280
|
+
self._is_grabbing = not was_grabbing
|
|
281
|
+
|
|
282
|
+
return None if was_grabbing else self._grab_data_list
|
|
283
|
+
|
|
284
|
+
def stop_grab(self):
|
|
285
|
+
self._is_grabbing = False
|
|
286
|
+
self.set_remote_name()
|
|
287
|
+
self._director.ask_rpc("stop_grab")
|
|
288
|
+
|
|
289
|
+
def set_data(self, data=None, additional_payload=None):
|
|
290
|
+
value: DataToExport = cast(DataToExport, sf.get_apply_deserializer(additional_payload[0]))
|
|
291
|
+
if self._is_grabbing:
|
|
292
|
+
if self._grab_data_list is not None:
|
|
293
|
+
self._grab_data_list.append(value)
|
|
294
|
+
else:
|
|
295
|
+
try:
|
|
296
|
+
self._snap_data_future.set_result(value)
|
|
297
|
+
self._snap_data_future = None
|
|
298
|
+
except (InvalidStateError, AttributeError):
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class LECODashboardWrapper(LECOBaseWrapper):
|
|
303
|
+
def __init__(self, **kwargs) -> None:
|
|
304
|
+
self._experiments_future: Optional[Future[list[str]]] = None
|
|
305
|
+
self._applied_experiment_future: Optional[Future[bool]] = None
|
|
306
|
+
self._states_future: Optional[Future[list[str]]] = None
|
|
307
|
+
self._applied_state_future: Optional[Future[bool]] = None
|
|
308
|
+
self._devices_list_future: Optional[Future[dict[str, list[str]]]] = None
|
|
309
|
+
|
|
310
|
+
super().__init__('dashboard', **kwargs)
|
|
311
|
+
|
|
312
|
+
self._listener.register_binary_rpc_method(self.send_devices, accept_binary_input=True)
|
|
313
|
+
self._listener.register_rpc_method(self.send_states)
|
|
314
|
+
self._listener.register_rpc_method(self.send_experiments)
|
|
315
|
+
self._listener.register_rpc_method(self.applied_experiment_done)
|
|
316
|
+
self._listener.register_rpc_method(self.applied_state_done)
|
|
317
|
+
|
|
318
|
+
def get_devices(self) -> Future[dict[str, list[str]]]:
|
|
319
|
+
future = Future()
|
|
320
|
+
self._devices_list_future = future
|
|
321
|
+
|
|
322
|
+
self.set_remote_name()
|
|
323
|
+
self._director.ask_rpc("get_devices")
|
|
324
|
+
|
|
325
|
+
return future
|
|
326
|
+
|
|
327
|
+
def get_states(self) -> Future[list[str]]:
|
|
328
|
+
future = Future()
|
|
329
|
+
self._states_future = future
|
|
330
|
+
|
|
331
|
+
self.set_remote_name()
|
|
332
|
+
self._director.ask_rpc("get_states")
|
|
333
|
+
|
|
334
|
+
return future
|
|
335
|
+
|
|
336
|
+
def apply_state(self, state: str) -> Future[bool]:
|
|
337
|
+
future = Future()
|
|
338
|
+
self._applied_state_future = future
|
|
339
|
+
|
|
340
|
+
self.set_remote_name()
|
|
341
|
+
self._director.ask_rpc("apply_state", state=state)
|
|
342
|
+
|
|
343
|
+
return future
|
|
344
|
+
|
|
345
|
+
def get_experiments(self) -> Future[list[str]]:
|
|
346
|
+
future = Future()
|
|
347
|
+
self._experiments_future = future
|
|
348
|
+
|
|
349
|
+
self.set_remote_name()
|
|
350
|
+
self._director.ask_rpc("get_experiments")
|
|
351
|
+
|
|
352
|
+
return future
|
|
353
|
+
|
|
354
|
+
def apply_experiment(self, experiment: str) -> Future[bool]:
|
|
355
|
+
future = Future()
|
|
356
|
+
self._applied_experiment_future = future
|
|
357
|
+
|
|
358
|
+
self.set_remote_name()
|
|
359
|
+
self._director.ask_rpc("apply_experiment", experiment=experiment)
|
|
360
|
+
|
|
361
|
+
return future
|
|
362
|
+
|
|
363
|
+
def send_devices(self, data=None, additional_payload=None):
|
|
364
|
+
value: dict[str, list[str]] = sf.get_apply_deserializer(additional_payload[0])
|
|
365
|
+
try:
|
|
366
|
+
self._devices_list_future.set_result(value)
|
|
367
|
+
self._devices_list_future = None
|
|
368
|
+
except (InvalidStateError, AttributeError):
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
def send_states(self, states: list[str]):
|
|
372
|
+
try:
|
|
373
|
+
self._states_future.set_result(states)
|
|
374
|
+
self._states_future = None
|
|
375
|
+
except (InvalidStateError, AttributeError):
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
def send_experiments(self, experiments: list[str]):
|
|
379
|
+
try:
|
|
380
|
+
self._experiments_future.set_result(experiments)
|
|
381
|
+
self._experiments_future = None
|
|
382
|
+
except (InvalidStateError, AttributeError):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
def applied_experiment_done(self, done: bool):
|
|
386
|
+
try:
|
|
387
|
+
self._applied_experiment_future.set_result(done)
|
|
388
|
+
self._applied_experiment_future = None
|
|
389
|
+
except (InvalidStateError, AttributeError):
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
def applied_state_done(self, done: bool):
|
|
393
|
+
try:
|
|
394
|
+
self._applied_state_future.set_result(done)
|
|
395
|
+
self._applied_state_future = None
|
|
396
|
+
except (InvalidStateError, AttributeError):
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
WrapperT = TypeVar('WrapperT', bound=LECOBaseWrapper)
|
|
400
|
+
|
|
401
|
+
class Device(ABC, Generic[WrapperT]):
|
|
402
|
+
def __init__(self, wrapper: WrapperT) -> None:
|
|
403
|
+
self._wrapper = wrapper
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def leco_name(self):
|
|
407
|
+
return self._wrapper.leco_name
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def name(self) -> str:
|
|
411
|
+
#Cast to avoid pycharm warning
|
|
412
|
+
return cast(str, cast(object, self._wrapper.name))
|
|
413
|
+
|
|
414
|
+
def sign_out(self) -> None:
|
|
415
|
+
self._wrapper.sign_out()
|