pts-framework 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pts_framework-0.6.1.dist-info/METADATA +95 -0
- pts_framework-0.6.1.dist-info/RECORD +60 -0
- pts_framework-0.6.1.dist-info/WHEEL +5 -0
- pts_framework-0.6.1.dist-info/entry_points.txt +3 -0
- pts_framework-0.6.1.dist-info/top_level.txt +1 -0
- pypts/XYGraph/StreamContainer.py +67 -0
- pypts/XYGraph/XY_graph.py +362 -0
- pypts/XYGraph/simulated_signals.py +88 -0
- pypts/YamVIEW/__init__.py +3 -0
- pypts/YamVIEW/customGUIModules.py +416 -0
- pypts/YamVIEW/recipe_creator.py +569 -0
- pypts/YamVIEW/recipe_sequencer_setup.py +523 -0
- pypts/YamVIEW/recipe_step_setup.py +577 -0
- pypts/YamVIEW/styles.py +13 -0
- pypts/YamVIEW/verify_recipe.py +105 -0
- pypts/__init__.py +41 -0
- pypts/__main__.py +43 -0
- pypts/_version.py +24 -0
- pypts/clim_chamber.py +41 -0
- pypts/common.py +11 -0
- pypts/dev_tests.py +82 -0
- pypts/event_proxy.py +226 -0
- pypts/example_tests.py +213 -0
- pypts/examples/environment_setup_tools/Minimal_setup/Minimal_setup_recipe.yml +212 -0
- pypts/examples/environment_setup_tools/Minimal_setup/README.md +41 -0
- pypts/examples/environment_setup_tools/Minimal_setup/init_env_min.py +46 -0
- pypts/examples/environment_setup_tools/Minimal_setup/tests/example_tests.py +86 -0
- pypts/examples/environment_setup_tools/Package_based_setup/Package_based_recipe.yml +215 -0
- pypts/examples/environment_setup_tools/Package_based_setup/README.md +55 -0
- pypts/examples/environment_setup_tools/Package_based_setup/init_env_pack.py +91 -0
- pypts/examples/environment_setup_tools/Package_based_setup/pyproject.toml +76 -0
- pypts/examples/environment_setup_tools/Package_based_setup/tests/example_tests.py +86 -0
- pypts/exceptions.py +3 -0
- pypts/gui.py +818 -0
- pypts/gui_components/__init__.py +5 -0
- pypts/gui_components/interaction_panel.py +206 -0
- pypts/gui_components/log_panel.py +53 -0
- pypts/gui_components/resources.py +87 -0
- pypts/gui_components/results_panel.py +162 -0
- pypts/gui_components/step_table.py +159 -0
- pypts/gui_components/styles.py +463 -0
- pypts/gui_components/toolbar.py +115 -0
- pypts/gui_theme.py +164 -0
- pypts/instruments/__init__.py +15 -0
- pypts/instruments/pendulum/__init__.py +7 -0
- pypts/instruments/pendulum/cnt91.py +122 -0
- pypts/pts.py +250 -0
- pypts/recipe.py +802 -0
- pypts/recipe_artifacts.py +85 -0
- pypts/recipe_language.py +389 -0
- pypts/recipe_parser.py +552 -0
- pypts/recipe_reference.py +217 -0
- pypts/recipes/shared_object_example.py +23 -0
- pypts/recipes/shared_resource_client.py +24 -0
- pypts/recipes/shared_resource_server.py +62 -0
- pypts/recipes/shared_resource_tests_example.py +31 -0
- pypts/report.py +544 -0
- pypts/startup.py +86 -0
- pypts/steps.py +1257 -0
- pypts/utils.py +290 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pts-framework
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Hardware-oriented testing framework developed by BE-CEM-MTA
|
|
5
|
+
Author-email: Odd Oyvind Andreassen <odd.oyvind.andreassen@cern.ch>, Bartlomiej Banas <Bartlomiej.Banas@cern.ch>, Oliver Damborg <odamborg@cern.ch>, Alvaro Martinez Landete <alvaro.martinez.landete@cern.ch>
|
|
6
|
+
License-Expression: LGPL-2.1-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/CERN/pts-framework
|
|
8
|
+
Project-URL: Repository, https://github.com/CERN/pts-framework
|
|
9
|
+
Project-URL: Changelog, https://github.com/CERN/pts-framework/blob/master/CHANGELOG.md
|
|
10
|
+
Keywords: testing,hardware,instrumentation,cern
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: hightime==0.2.2
|
|
19
|
+
Requires-Dist: matplotlib
|
|
20
|
+
Requires-Dist: nidmm==1.4.8
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: PySide6==6.9.1
|
|
23
|
+
Requires-Dist: PyYAML==6.0.2
|
|
24
|
+
Requires-Dist: ruamel.yaml
|
|
25
|
+
Requires-Dist: pymeasure==0.15.0
|
|
26
|
+
Requires-Dist: pyserial
|
|
27
|
+
Requires-Dist: paramiko
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.13
|
|
29
|
+
Requires-Dist: pyvisa
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-qt; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-timeout; extra == "test"
|
|
35
|
+
Provides-Extra: doc
|
|
36
|
+
Requires-Dist: Sphinx; extra == "doc"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pts-framework[doc,test]; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff; extra == "dev"
|
|
40
|
+
|
|
41
|
+
<!--
|
|
42
|
+
SPDX-FileCopyrightText: 2025 CERN <home.cern>
|
|
43
|
+
|
|
44
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
45
|
+
-->
|
|
46
|
+
|
|
47
|
+
# pypts
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
Hardware-oriented testing framework developed by BE-CEM-MTA.
|
|
52
|
+
|
|
53
|
+
## Key Features
|
|
54
|
+
|
|
55
|
+
* **YAML-based Recipes:** Define test sequences, steps, parameters, and variables using structured YAML files.
|
|
56
|
+
* **Modular Steps:** Supports various step types including Python function calls, sub-sequence execution, user interaction prompts, wait times, and indexed steps for running tasks multiple times with varying inputs.
|
|
57
|
+
**Reliable order execution:** Unlike pytest-order, the reliably follows the specified order.
|
|
58
|
+
* **Variable Scopes:** Manages global variables for the recipe and local variables within sequences.
|
|
59
|
+
* **Threaded Execution:** Runs recipes in a separate thread using `pts.run_pts`.
|
|
60
|
+
* **Incremental Reporting:** Generates a detailed CSV report (`report.csv`) in real-time as steps complete.
|
|
61
|
+
* **Contextual Reports:** Includes run context (Recipe Name, File Name, Serial Number) and step context (Sequence Name) in reports.
|
|
62
|
+
* **HTML Reports:** Provides a utility to convert the CSV report into a styled HTML file (`report.html`) for easy viewing, similar to `pytest-html`.
|
|
63
|
+
* **Event System:** Uses queues for inter-thread communication and event reporting (e.g., step start/end).
|
|
64
|
+
|
|
65
|
+
For full documentation, please visit [PTS Framework Documentation](https://acc-py.web.cern.ch/gitlab/pts/framework/pypts/docs/stable/).
|
|
66
|
+
|
|
67
|
+
=======
|
|
68
|
+
|
|
69
|
+
## Continuous integration and releases
|
|
70
|
+
|
|
71
|
+
The project is tested in a disposable Python 3.13 Docker image. To reproduce the
|
|
72
|
+
GitHub CI build locally, run:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
docker build --build-arg SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=0+local -t pts-framework-ci .
|
|
76
|
+
docker run --rm pts-framework-ci
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Creating a protected `vX.Y.Z` tag runs the same tests, builds the wheel and source
|
|
80
|
+
distribution, and publishes them to TestPyPI. Verify the TestPyPI package in a clean
|
|
81
|
+
environment before approving the `pypi` GitHub Environment:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pts-framework==X.Y.Z
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Approval publishes the same distributions to PyPI. GitHub retains the release
|
|
88
|
+
artifact for 30 days; PyPI is the permanent package source. See
|
|
89
|
+
[release configuration](.github/RELEASE.md) for the one-time setup.
|
|
90
|
+
|
|
91
|
+
## Licences
|
|
92
|
+
|
|
93
|
+
pypts is distributed under the **LGPL-2.1-or-later** licence.
|
|
94
|
+
|
|
95
|
+
pypts comes with a [dependency licence compatibility analysis report](https://acc-py.web.cern.ch/gitlab/pts/framework/pypts/docs/stable/dependency_license_analysis.html).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
pypts/__init__.py,sha256=oEg-uNlPR0aNLSShZTPw1DIm8pC_fX1FdGrsOVn8LeY,1419
|
|
2
|
+
pypts/__main__.py,sha256=0ofxZWPeZ1MEmYbJuXGAJtWbO8E9MsJ2cOKnob45WcU,1370
|
|
3
|
+
pypts/_version.py,sha256=G-hFKu7J8TJGYR0b0qkq_gYjJT0RCbJrht_uMdajMP8,520
|
|
4
|
+
pypts/clim_chamber.py,sha256=JOUHpLUrDjck2p_tgBwmfmpANeg7E-jlDFwmJgWvVzw,1386
|
|
5
|
+
pypts/common.py,sha256=dq_q1QnUdqIoNC3QFv0jjzpipbGffDNviA1hzpvoEHs,349
|
|
6
|
+
pypts/dev_tests.py,sha256=kjbfHVmaOOAV79vgWlZm3IQC9iU6mJ7Cs3C7BGYQ38Q,2822
|
|
7
|
+
pypts/event_proxy.py,sha256=s-QYVzoUOJJnPpSU42IB9OAsLCLbUs0QsfQ1fuvJxMk,10631
|
|
8
|
+
pypts/example_tests.py,sha256=Y8PvWlp1oehkB67JZ9vpVPvpMlZ0Ml3thN4XerJGX0k,7156
|
|
9
|
+
pypts/exceptions.py,sha256=VoLUKCt_ktavZgoG4-byyjYKJC2Iuvd3IvAyuBEN7qg,95
|
|
10
|
+
pypts/gui.py,sha256=DmHULwK4qmd9A6e1M4nfL6hw8JQMlewrgZ04dHpZAFM,30791
|
|
11
|
+
pypts/gui_theme.py,sha256=9n2QfX-WNfN83-82X9DEGetsBCajoPnC2nSt2-6zV5k,4607
|
|
12
|
+
pypts/pts.py,sha256=oMITDSX_qUuzMYWJrq56dDqvnApMcl3JlNDr1mY53ak,9933
|
|
13
|
+
pypts/recipe.py,sha256=Nd8cgJjnvJ1KgjNkK_ftleelrsSFfbt4QBsyxuh5zGk,33414
|
|
14
|
+
pypts/recipe_artifacts.py,sha256=BQayxHacnEvH0UqQN4j-uNomIDcy8oe-dGznBlGBx_4,2993
|
|
15
|
+
pypts/recipe_language.py,sha256=PjmjGCFNigklKx-T9mY0EWSOnCphK3n7LBK2RNlev9M,14062
|
|
16
|
+
pypts/recipe_parser.py,sha256=iq11qkbAHa4lr_igrMcoqO2WVxS8F0Ous9A_yWMeTMM,20177
|
|
17
|
+
pypts/recipe_reference.py,sha256=G55IVNcpoyAg3Ii_BhSDArG7AihenRlRji3NrT09muA,7753
|
|
18
|
+
pypts/report.py,sha256=Ucim3Ldxr6n7HPNFC773KpmUGhJ9yU1h0Frl37qMuHo,22618
|
|
19
|
+
pypts/startup.py,sha256=izcN85izG3cvKsQ231XFOswbilkhIHvaihUyWusPjj4,2909
|
|
20
|
+
pypts/steps.py,sha256=3_1K_UFzAJ39DwraVCsE0YSSBPt4Nvc4tbB7VlsPxPI,63178
|
|
21
|
+
pypts/utils.py,sha256=rOhVepQeNqD0GDaH776SutG9O86W-yRR67K1AhHy94w,10353
|
|
22
|
+
pypts/XYGraph/StreamContainer.py,sha256=VjpPSjHT_9JbuEWfZh5yRv5M9-DNMc7vKOD2I1Al6D4,2108
|
|
23
|
+
pypts/XYGraph/XY_graph.py,sha256=plCdyFdMHYHx7gO-K33wuuRqLRXNUHwlItuLJUUOWYU,13926
|
|
24
|
+
pypts/XYGraph/simulated_signals.py,sha256=Totv-BzoCBn-RRV401-xrSJh8BtG2BmETv3fo72aEO4,3362
|
|
25
|
+
pypts/YamVIEW/__init__.py,sha256=VoLUKCt_ktavZgoG4-byyjYKJC2Iuvd3IvAyuBEN7qg,95
|
|
26
|
+
pypts/YamVIEW/customGUIModules.py,sha256=kbyW_UsQpvX5wRCBQGRdKxzbAH_i1EQQ8SBV_7Tk9SQ,15296
|
|
27
|
+
pypts/YamVIEW/recipe_creator.py,sha256=BHDsdAgYgH8EJDr6exfLJBMJigPYKt4fEz7FbenPmx0,22821
|
|
28
|
+
pypts/YamVIEW/recipe_sequencer_setup.py,sha256=S1k_Er_I58yLEDx0LErRWQCfH5yll3M_GT1bJXTxTIY,20159
|
|
29
|
+
pypts/YamVIEW/recipe_step_setup.py,sha256=ZG7vQ-0Wq-vnxKPlwzBiD41MGIqvZNkWXAjQP5Hybj8,22948
|
|
30
|
+
pypts/YamVIEW/styles.py,sha256=ZD9jpmP2xfEfOOgjUIsobGr6rUlk2FBScNnCdihKykA,348
|
|
31
|
+
pypts/YamVIEW/verify_recipe.py,sha256=K9mOtd4gvbkNxDMTZ2btWPMlvyBa1xQtVa1xVD4SNz4,3809
|
|
32
|
+
pypts/examples/environment_setup_tools/Minimal_setup/Minimal_setup_recipe.yml,sha256=-AVYo2zFwp84JvHMJEhnEbFxZy2H6lCVwFa1rkcs1NI,5601
|
|
33
|
+
pypts/examples/environment_setup_tools/Minimal_setup/README.md,sha256=n1GIGqJyErN337ApLW2aySW_-6JHH9kzXg9Klp04f9E,2005
|
|
34
|
+
pypts/examples/environment_setup_tools/Minimal_setup/init_env_min.py,sha256=95mXS-N-NtVxN5WG6fP2JFUafuaPurSFdGx3aCv6HWA,1613
|
|
35
|
+
pypts/examples/environment_setup_tools/Minimal_setup/tests/example_tests.py,sha256=dSEONBL2RK7nOxnqTE2584mrmiSuF4fngtkLPXMu0SI,2622
|
|
36
|
+
pypts/examples/environment_setup_tools/Package_based_setup/Package_based_recipe.yml,sha256=Pc4E3LILB1KndKbT3lsroanrvZV7lCkhcZL6PU6Je7g,6050
|
|
37
|
+
pypts/examples/environment_setup_tools/Package_based_setup/README.md,sha256=Popcq7qQuq3lFQgYwFg5gS-OLaXE127eyaG1y9eeGG8,2651
|
|
38
|
+
pypts/examples/environment_setup_tools/Package_based_setup/init_env_pack.py,sha256=uR_7MGtFPt7dGfRsnlVssS7aFd0TT3SlpM2llH4GE30,3008
|
|
39
|
+
pypts/examples/environment_setup_tools/Package_based_setup/pyproject.toml,sha256=x9zN5D9iQ4mDCEzOOLHv2eHi5kz0liWA9MMhlt0ZVfE,2198
|
|
40
|
+
pypts/examples/environment_setup_tools/Package_based_setup/tests/example_tests.py,sha256=dSEONBL2RK7nOxnqTE2584mrmiSuF4fngtkLPXMu0SI,2622
|
|
41
|
+
pypts/gui_components/__init__.py,sha256=WtEA2hD83IkhGu7n-F788fC-owbOeQy6HmI2NI3zuWM,161
|
|
42
|
+
pypts/gui_components/interaction_panel.py,sha256=HnI89xd5Rn8l7fujM6B4MX7MiFQQMK9Sn8PzkODzmYo,8046
|
|
43
|
+
pypts/gui_components/log_panel.py,sha256=I3FmHQG0NDIthb-wSBvyQq3JBrwLNcTNEE8T01SqAdg,1780
|
|
44
|
+
pypts/gui_components/resources.py,sha256=EPUiCC_35-OW5cKzPlt1Qwo2drhEQA5foD8gi8XPCS4,2645
|
|
45
|
+
pypts/gui_components/results_panel.py,sha256=zWTK2Y4DUDp1tvNxQdM_SDVXmkJBHzedOTDIf9wiygw,6154
|
|
46
|
+
pypts/gui_components/step_table.py,sha256=s3HYzWNoDviIhnl1udPWFtncK-l8OyRoc_tvgecWmwE,6257
|
|
47
|
+
pypts/gui_components/styles.py,sha256=FVjrGy7d8s7-ZxG-bMq5h2JdTbD0BUOSDkKVYBRCEhQ,10211
|
|
48
|
+
pypts/gui_components/toolbar.py,sha256=Os7JvBFeVRuI64z9DSR8XV61kMTbhuS-JUfcQxqJSVQ,3959
|
|
49
|
+
pypts/instruments/__init__.py,sha256=C3e8GmKtqpsWE_ojqzSUmt7ghoqAV6OFKAEznzuQJRg,530
|
|
50
|
+
pypts/instruments/pendulum/__init__.py,sha256=rBeXfYkSWLtoKUaM55HSh9wZtsamNGQGL5MHnfW5JiQ,142
|
|
51
|
+
pypts/instruments/pendulum/cnt91.py,sha256=IRSHDXXZqhxjjmQ0Ns2X2fR4QbM5nXw2WoIfcfuSLdc,4757
|
|
52
|
+
pypts/recipes/shared_object_example.py,sha256=y9_4iMbbh1XvgmsQFNhwB29hgB6rE1VBLHiXclfTwAU,580
|
|
53
|
+
pypts/recipes/shared_resource_client.py,sha256=2Ybj_NytymyoGKbWzFD24qPOd33SARfhLDE59R_M5_k,762
|
|
54
|
+
pypts/recipes/shared_resource_server.py,sha256=LX6X73z4Ie4v2aViWWI9DQooXJeCvL7pxrW4rvhqxaE,1964
|
|
55
|
+
pypts/recipes/shared_resource_tests_example.py,sha256=lYo55xHrjCaEoC_hjiJSODnyaoXOoNd4et1tf7nLrBY,739
|
|
56
|
+
pts_framework-0.6.1.dist-info/METADATA,sha256=wfqMXdhw1YCZ1jApIFIehayQQ2rSAszEw4s5AB73W1M,4246
|
|
57
|
+
pts_framework-0.6.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
58
|
+
pts_framework-0.6.1.dist-info/entry_points.txt,sha256=8YNA0KgYuIGtYzIIqC9ZjQE0S_N8AiQ5h2oh16nBWt4,198
|
|
59
|
+
pts_framework-0.6.1.dist-info/top_level.txt,sha256=f88XKIMCKDHJYS2jF4YkUc3F3VG3BoZysKiCNbQOuv4,6
|
|
60
|
+
pts_framework-0.6.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pypts
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 CERN (home.cern)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
4
|
+
|
|
5
|
+
class GlobalContainer:
|
|
6
|
+
_instance = None
|
|
7
|
+
def __new__(cls, *args, **kwargs):
|
|
8
|
+
|
|
9
|
+
print("Something is creating the container object")
|
|
10
|
+
if cls._instance is None:
|
|
11
|
+
cls._instance = super().__new__(cls)
|
|
12
|
+
cls._instance.streamlist = []
|
|
13
|
+
return cls._instance
|
|
14
|
+
|
|
15
|
+
def remove_stream(self, Stream):
|
|
16
|
+
self.streamlist.remove(Stream)
|
|
17
|
+
|
|
18
|
+
def add_stream(self, Stream):
|
|
19
|
+
self.streamlist.append(Stream)
|
|
20
|
+
|
|
21
|
+
def get_stream(self, key):
|
|
22
|
+
return self.streamlist[key]
|
|
23
|
+
|
|
24
|
+
def get_all_streams(self):
|
|
25
|
+
return list(self.streamlist)
|
|
26
|
+
|
|
27
|
+
def get_streams_info(self):
|
|
28
|
+
streams_info = ""
|
|
29
|
+
print(f"container object inside the streams_info: {container}")
|
|
30
|
+
for stream in self.streamlist:
|
|
31
|
+
streams_info = streams_info + (f"Retrieved registered stream {stream.name}. Stream is tied with {stream.hook} hook.\n")
|
|
32
|
+
return streams_info
|
|
33
|
+
|
|
34
|
+
global container
|
|
35
|
+
container = GlobalContainer()
|
|
36
|
+
|
|
37
|
+
class Stream:
|
|
38
|
+
def __init__(self, name, hook, description=None, unit=None, frequency=None):
|
|
39
|
+
self.name = name
|
|
40
|
+
self.hook = hook
|
|
41
|
+
self.description = description
|
|
42
|
+
self.unit = unit
|
|
43
|
+
self.frequency = frequency
|
|
44
|
+
container.add_stream(self)
|
|
45
|
+
|
|
46
|
+
def kill(self):
|
|
47
|
+
container.remove_stream(self)
|
|
48
|
+
del(self)
|
|
49
|
+
#
|
|
50
|
+
# if __name__ == "__main__":
|
|
51
|
+
# stream1 = Stream(name = "PT100 probe", hook = "file1.csv")
|
|
52
|
+
# stream2 = Stream(name = "oscilloscope", hook = "file2.csv")
|
|
53
|
+
#
|
|
54
|
+
# # container = GlobalContainer()
|
|
55
|
+
# # print("Adding 1st stream")
|
|
56
|
+
# # container.add_stream(stream1)
|
|
57
|
+
# # print(container.get_streams_info())
|
|
58
|
+
# # print("Adding 2nd stream")
|
|
59
|
+
# # container.add_stream(stream2)
|
|
60
|
+
# # print(container.get_streams_info())
|
|
61
|
+
#
|
|
62
|
+
# import time
|
|
63
|
+
# while True:
|
|
64
|
+
# time.sleep(1)
|
|
65
|
+
# streamlist = container.get_all_streams()
|
|
66
|
+
# for stream in streamlist:
|
|
67
|
+
# print(f"Retrieved registered stream {stream.name}. Stream is tied with {stream.hook} hook.")
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 CERN (home.cern)
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import sys, os
|
|
7
|
+
import random, time
|
|
8
|
+
from PySide6 import QtWidgets, QtCore, QtGui
|
|
9
|
+
from PySide6.QtCore import QDateTime
|
|
10
|
+
from pyqtgraph import PlotWidget, plot
|
|
11
|
+
import pyqtgraph as pg
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import csv
|
|
14
|
+
from pypts.XYGraph.StreamContainer import *
|
|
15
|
+
from pypts.XYGraph.simulated_signals import *
|
|
16
|
+
|
|
17
|
+
global container
|
|
18
|
+
|
|
19
|
+
class SignalSpawner(QtWidgets.QMainWindow):
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super(SignalSpawner, self).__init__(*args, **kwargs)
|
|
22
|
+
self.signal_dictionary = {}
|
|
23
|
+
|
|
24
|
+
# Main widget and layout
|
|
25
|
+
self.setWindowTitle("Signal Management")
|
|
26
|
+
self.setGeometry(1000, 100, 400, 300)
|
|
27
|
+
central_widget = QtWidgets.QWidget()
|
|
28
|
+
self.setCentralWidget(central_widget)
|
|
29
|
+
main_layout = QtWidgets.QHBoxLayout(central_widget) # Split: left (controls), right (list)
|
|
30
|
+
|
|
31
|
+
# LEFT side: controls
|
|
32
|
+
control_layout = QtWidgets.QVBoxLayout()
|
|
33
|
+
main_layout.addLayout(control_layout)
|
|
34
|
+
|
|
35
|
+
self.signal_name = QtWidgets.QLineEdit()
|
|
36
|
+
self.signal_name.setText("Signal1")
|
|
37
|
+
control_layout.addWidget(self.signal_name)
|
|
38
|
+
|
|
39
|
+
self.startButton = QtWidgets.QPushButton("Start signal generation")
|
|
40
|
+
self.startButton.clicked.connect(self.start_simulated_acquisition)
|
|
41
|
+
control_layout.addWidget(self.startButton)
|
|
42
|
+
|
|
43
|
+
self.stopButton = QtWidgets.QPushButton("Stop signal generation")
|
|
44
|
+
self.stopButton.clicked.connect(self.stop_simulated_acquisition)
|
|
45
|
+
control_layout.addWidget(self.stopButton)
|
|
46
|
+
|
|
47
|
+
# Add "Open File" button
|
|
48
|
+
self.openFileButton = QtWidgets.QPushButton("Open file")
|
|
49
|
+
self.openFileButton.clicked.connect(self.open_file_dialog)
|
|
50
|
+
control_layout.addWidget(self.openFileButton)
|
|
51
|
+
|
|
52
|
+
control_layout.addStretch()
|
|
53
|
+
|
|
54
|
+
# RIGHT side: list of active signals
|
|
55
|
+
self.signal_list = QtWidgets.QListWidget()
|
|
56
|
+
main_layout.addWidget(self.signal_list)
|
|
57
|
+
|
|
58
|
+
# Double-click on a signal in the list to stop it
|
|
59
|
+
self.signal_list.itemDoubleClicked.connect(self.select_signal_from_the_list)
|
|
60
|
+
|
|
61
|
+
def open_file_dialog(self):
|
|
62
|
+
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
|
63
|
+
self,
|
|
64
|
+
"Select a file",
|
|
65
|
+
"", # start directory
|
|
66
|
+
"All Files (*)" # filter
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if file_path:
|
|
70
|
+
file_name_no_ext = os.path.splitext(os.path.basename(file_path))[0]
|
|
71
|
+
self.signal_name.setText(file_name_no_ext)
|
|
72
|
+
self.load_stream_from_file(file_name=file_name_no_ext, file_path=file_path)
|
|
73
|
+
|
|
74
|
+
def load_stream_from_file(self, file_name, file_path):
|
|
75
|
+
new_signal = Stream(file_name, file_path)
|
|
76
|
+
self.signal_dictionary[file_name] = new_signal
|
|
77
|
+
|
|
78
|
+
# Add to list widget
|
|
79
|
+
self.signal_list.addItem(file_name)
|
|
80
|
+
|
|
81
|
+
def select_signal_from_the_list(self, item):
|
|
82
|
+
self.signal_name.setText(item.text())
|
|
83
|
+
|
|
84
|
+
def start_simulated_acquisition(self):
|
|
85
|
+
name = self.signal_name.text()
|
|
86
|
+
if name in self.signal_dictionary:
|
|
87
|
+
QtWidgets.QMessageBox.warning(self, "Warning", f"Signal '{name}' already exists!")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
new_signal = Simulated_sine_wave(randomize=True, name=name)
|
|
91
|
+
self.signal_dictionary[name] = new_signal
|
|
92
|
+
new_signal.start_acquisition()
|
|
93
|
+
|
|
94
|
+
# Add to list widget
|
|
95
|
+
self.signal_list.addItem(name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def stop_simulated_acquisition(self):
|
|
99
|
+
name = self.signal_name.text()
|
|
100
|
+
if name in self.signal_dictionary:
|
|
101
|
+
self.signal_dictionary[name].stop_acquisition()
|
|
102
|
+
del self.signal_dictionary[name]
|
|
103
|
+
|
|
104
|
+
# Remove from list widget
|
|
105
|
+
items = self.signal_list.findItems(name, QtCore.Qt.MatchFlag.MatchExactly)
|
|
106
|
+
for item in items:
|
|
107
|
+
self.signal_list.takeItem(self.signal_list.row(item))
|
|
108
|
+
|
|
109
|
+
class plottable_data():
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.timestamps = None
|
|
112
|
+
self.datapoints = None
|
|
113
|
+
|
|
114
|
+
class PlotWindow(QtWidgets.QWidget):
|
|
115
|
+
def __init__(self, *args, **kwargs):
|
|
116
|
+
super(PlotWindow, self).__init__(*args, **kwargs)
|
|
117
|
+
self.selected_stream = None
|
|
118
|
+
self.stream_list = []
|
|
119
|
+
self.plottable_signal = plottable_data()
|
|
120
|
+
# Main widget and layout
|
|
121
|
+
central_widget = QtWidgets.QWidget()
|
|
122
|
+
# self.setCentralWidget(central_widget)
|
|
123
|
+
self.main_layout = QtWidgets.QHBoxLayout(self)
|
|
124
|
+
# Create three vertical layouts the main
|
|
125
|
+
self.vertical_layout_1 = QtWidgets.QVBoxLayout()
|
|
126
|
+
self.vertical_layout_2 = QtWidgets.QVBoxLayout()
|
|
127
|
+
self.vertical_layout_3 = QtWidgets.QVBoxLayout()
|
|
128
|
+
# Add the vertical layouts to the main horizontal layout
|
|
129
|
+
self.main_layout.addLayout(self.vertical_layout_1)
|
|
130
|
+
self.main_layout.addLayout(self.vertical_layout_2)
|
|
131
|
+
self.main_layout.addLayout(self.vertical_layout_3)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Default maximum data points shown
|
|
135
|
+
self.max_points = 10000
|
|
136
|
+
|
|
137
|
+
self.setup_plot_area()
|
|
138
|
+
|
|
139
|
+
self.first_plot = self.graphWidget.plot([], [], pen=pg.mkPen('r', width=2))
|
|
140
|
+
self.setup_stream_selector()
|
|
141
|
+
# Data points knob and label
|
|
142
|
+
self.points_label = QtWidgets.QLabel(f"Data Points: {self.max_points}")
|
|
143
|
+
self.setup_timestamp_knob()
|
|
144
|
+
self.setup_autoscale_checkbox()
|
|
145
|
+
self.setup_show_all_data_checkbox()
|
|
146
|
+
self.setup_config_button()
|
|
147
|
+
self.setup_timer()
|
|
148
|
+
self.setup_datetime_label()
|
|
149
|
+
|
|
150
|
+
self.vertical_layout_1.addWidget(self.graphWidget)
|
|
151
|
+
self.vertical_layout_2.addWidget(self.plot_selector)
|
|
152
|
+
self.vertical_layout_2.addWidget(self.points_label)
|
|
153
|
+
self.vertical_layout_2.addWidget(self.dial)
|
|
154
|
+
self.vertical_layout_2.addWidget(self.autoscale_Y_checkbox)
|
|
155
|
+
self.vertical_layout_2.addWidget(self.autoscale_X_checkbox)
|
|
156
|
+
self.vertical_layout_2.addWidget(self.show_all_checkbox)
|
|
157
|
+
self.vertical_layout_2.addWidget(self.config_button)
|
|
158
|
+
self.vertical_layout_2.addStretch()
|
|
159
|
+
self.vertical_layout_2.addWidget(self.datetime_label)
|
|
160
|
+
|
|
161
|
+
# GUI related setup methods
|
|
162
|
+
def setup_plot_area(self):
|
|
163
|
+
# Create the plot widget and add it to the vertical1 layout
|
|
164
|
+
self.graphWidget = pg.PlotWidget()
|
|
165
|
+
self.graphWidget.showGrid(x=True, y=True, alpha=20)
|
|
166
|
+
self.graphWidget.setBackground('w')
|
|
167
|
+
|
|
168
|
+
def setup_stream_selector(self):
|
|
169
|
+
# Drop-down menu to select one plot
|
|
170
|
+
self.plot_selector = QtWidgets.QComboBox()
|
|
171
|
+
self.plot_selector.setEditable(True)
|
|
172
|
+
self.plot_selector.setFixedWidth(200)
|
|
173
|
+
self.plot_selector.setCurrentIndex(-1)
|
|
174
|
+
# self.plot_selector.currentIndexChanged.connect(self.select_plot)
|
|
175
|
+
self.plot_selector.activated.connect(self.select_plot)
|
|
176
|
+
|
|
177
|
+
def setup_autoscale_checkbox(self):
|
|
178
|
+
# Autoscale checkbox Y
|
|
179
|
+
self.autoscale_Y_checkbox = QtWidgets.QCheckBox("Autoscale Y-Axis")
|
|
180
|
+
self.autoscale_Y_checkbox.setChecked(True)
|
|
181
|
+
self.autoscale_Y_checkbox.stateChanged.connect(self.toggle_autoscale_Y)
|
|
182
|
+
# Autoscale checkbox X
|
|
183
|
+
self.autoscale_X_checkbox = QtWidgets.QCheckBox("Autoscale X-Axis")
|
|
184
|
+
self.autoscale_X_checkbox.setChecked(True)
|
|
185
|
+
self.autoscale_X_checkbox.stateChanged.connect(self.toggle_autoscale_X)
|
|
186
|
+
|
|
187
|
+
def setup_show_all_data_checkbox(self):
|
|
188
|
+
# Show all data checkbox
|
|
189
|
+
self.show_all_checkbox = QtWidgets.QCheckBox("Show All Data")
|
|
190
|
+
self.show_all_checkbox.setChecked(False)
|
|
191
|
+
self.show_all_checkbox.stateChanged.connect(self.toggle_show_all)
|
|
192
|
+
|
|
193
|
+
def setup_config_button(self):
|
|
194
|
+
# Configuration button
|
|
195
|
+
self.config_button = QtWidgets.QPushButton("Configuration")
|
|
196
|
+
self.config_button.setDisabled(True)
|
|
197
|
+
|
|
198
|
+
def setup_datetime_label(self):
|
|
199
|
+
# datetime refresh label
|
|
200
|
+
self.datetime_label = QtWidgets.QLabel()
|
|
201
|
+
self.datetime_label.setStyleSheet("font-size: 8px; font-weight: bold;")
|
|
202
|
+
|
|
203
|
+
def setup_timestamp_knob(self):
|
|
204
|
+
#TODO - store those values in config
|
|
205
|
+
self.dial = QtWidgets.QDial()
|
|
206
|
+
self.dial.setRange(10, self.max_points)
|
|
207
|
+
self.dial.setValue(self.max_points)
|
|
208
|
+
self.dial.setSingleStep(5)
|
|
209
|
+
self.dial.valueChanged.connect(self.update_points_label)
|
|
210
|
+
|
|
211
|
+
def setup_timer(self):
|
|
212
|
+
# Timer updating every 100ms
|
|
213
|
+
self.timer100 = QtCore.QTimer()
|
|
214
|
+
self.timer100.setInterval(100)
|
|
215
|
+
self.timer100.timeout.connect(self.update_plot_data)
|
|
216
|
+
self.timer100.start()
|
|
217
|
+
|
|
218
|
+
# Timer updating every 1000ms
|
|
219
|
+
self.timer1000 = QtCore.QTimer()
|
|
220
|
+
self.timer1000.setInterval(1000)
|
|
221
|
+
self.timer1000.timeout.connect(self.update_datetime)
|
|
222
|
+
self.timer1000.timeout.connect(self.find_registered_streams)
|
|
223
|
+
self.timer1000.start()
|
|
224
|
+
|
|
225
|
+
# Freeze button
|
|
226
|
+
self.freeze_button = QtWidgets.QPushButton("Freeze")
|
|
227
|
+
self.freeze_button.setCheckable(True) # Make it toggleable
|
|
228
|
+
self.freeze_button.clicked.connect(self.toggle_freeze)
|
|
229
|
+
self.vertical_layout_2.addWidget(self.freeze_button)
|
|
230
|
+
self.is_frozen = False
|
|
231
|
+
|
|
232
|
+
# GUI action methods
|
|
233
|
+
def toggle_freeze(self):
|
|
234
|
+
self.is_frozen = self.freeze_button.isChecked()
|
|
235
|
+
if self.is_frozen:
|
|
236
|
+
self.freeze_button.setText("Resume")
|
|
237
|
+
else:
|
|
238
|
+
self.freeze_button.setText("Freeze")
|
|
239
|
+
|
|
240
|
+
def toggle_autoscale_Y(self):
|
|
241
|
+
# Enable or disable Y-axis autoscaling
|
|
242
|
+
if self.autoscale_Y_checkbox.isChecked():
|
|
243
|
+
self.graphWidget.enableAutoRange(axis=pg.ViewBox.YAxis)
|
|
244
|
+
else:
|
|
245
|
+
self.graphWidget.disableAutoRange(axis=pg.ViewBox.YAxis)
|
|
246
|
+
|
|
247
|
+
def toggle_autoscale_X(self):
|
|
248
|
+
# Enable or disable X-axis autoscaling
|
|
249
|
+
if self.autoscale_X_checkbox.isChecked():
|
|
250
|
+
self.graphWidget.enableAutoRange(axis=pg.ViewBox.XAxis)
|
|
251
|
+
else:
|
|
252
|
+
self.graphWidget.disableAutoRange(axis=pg.ViewBox.XAxis)
|
|
253
|
+
|
|
254
|
+
def toggle_show_all(self):
|
|
255
|
+
# Refresh plot to show all data or apply max_points
|
|
256
|
+
self.update_plot_data()
|
|
257
|
+
|
|
258
|
+
def update_plot_data(self):
|
|
259
|
+
if self.selected_stream == None:
|
|
260
|
+
return # do nothing if no plot is selected
|
|
261
|
+
|
|
262
|
+
if self.is_frozen:
|
|
263
|
+
return # Do nothing if frozen
|
|
264
|
+
|
|
265
|
+
timestamps, datapoints = self.load_csv_data(self.selected_stream)
|
|
266
|
+
self.plottable_signal.timestamps = self.timestamps_to_seconds(timestamps)
|
|
267
|
+
self.plottable_signal.datapoints = datapoints
|
|
268
|
+
|
|
269
|
+
if self.show_all_checkbox.isChecked():
|
|
270
|
+
self.first_plot.setData(self.plottable_signal.timestamps, self.plottable_signal.datapoints)
|
|
271
|
+
else:
|
|
272
|
+
self.first_plot.setData(self.plottable_signal.timestamps[-self.max_points:],
|
|
273
|
+
self.plottable_signal.datapoints[-self.max_points:])
|
|
274
|
+
|
|
275
|
+
def select_plot(self):
|
|
276
|
+
if (self.plot_selector.currentText() == ""):
|
|
277
|
+
pass
|
|
278
|
+
else:
|
|
279
|
+
self.selected_stream = self.find_stream_by_name(self.plot_selector.currentText())
|
|
280
|
+
|
|
281
|
+
def update_points_label(self, value):
|
|
282
|
+
# Update data points label and limit the data displayed
|
|
283
|
+
self.max_points = value
|
|
284
|
+
self.points_label.setText(f"Data Points: {value}")
|
|
285
|
+
self.update_plot_data()
|
|
286
|
+
|
|
287
|
+
def update_plot_selector_combobox(self):
|
|
288
|
+
current_text = self.plot_selector.currentText()
|
|
289
|
+
self.plot_selector.clear()
|
|
290
|
+
for stream in self.stream_list:
|
|
291
|
+
self.plot_selector.addItem(stream.name)
|
|
292
|
+
|
|
293
|
+
index = self.plot_selector.findText(current_text)
|
|
294
|
+
if index != -1:
|
|
295
|
+
self.plot_selector.setCurrentIndex(index)
|
|
296
|
+
|
|
297
|
+
# Stream handling related methods
|
|
298
|
+
def load_csv_data(self, stream):
|
|
299
|
+
file_path = stream.hook
|
|
300
|
+
timestamps = []
|
|
301
|
+
signals = []
|
|
302
|
+
with open(file_path, 'r') as file:
|
|
303
|
+
reader = csv.DictReader(file)
|
|
304
|
+
for row in reader:
|
|
305
|
+
timestamps.append(datetime.strptime(row['Timestamp'], "%Y-%m-%d %H:%M:%S.%f"))
|
|
306
|
+
signals.append(float(row['Signal']))
|
|
307
|
+
return timestamps, signals
|
|
308
|
+
|
|
309
|
+
def update_datetime(self):
|
|
310
|
+
current_datetime = QDateTime.currentDateTime().toString("yyyy-MM-dd HH:mm:ss")
|
|
311
|
+
self.datetime_label.setText(f"{current_datetime}")
|
|
312
|
+
|
|
313
|
+
def timestamps_to_seconds(self, timestamps):
|
|
314
|
+
start_time = timestamps[0]
|
|
315
|
+
return [(ts - start_time).total_seconds() for ts in timestamps]
|
|
316
|
+
|
|
317
|
+
def find_stream_by_name(self, stream_name):
|
|
318
|
+
for stream in self.stream_list:
|
|
319
|
+
if stream.name == stream_name:
|
|
320
|
+
return stream
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
def find_registered_streams(self):
|
|
324
|
+
self.stream_list = container.get_all_streams()
|
|
325
|
+
if (self.did_registered_streams_changed):
|
|
326
|
+
self.update_plot_selector_combobox()
|
|
327
|
+
|
|
328
|
+
if (self.stream_list):
|
|
329
|
+
if (self.selected_stream == None):
|
|
330
|
+
self.select_plot()
|
|
331
|
+
else:
|
|
332
|
+
self.plot_selector.setCurrentText("")
|
|
333
|
+
self.selected_stream = None
|
|
334
|
+
|
|
335
|
+
def did_registered_streams_changed(self):
|
|
336
|
+
current_steamlist = self.stream_list
|
|
337
|
+
new_streamlist = container.get_all_streams()
|
|
338
|
+
|
|
339
|
+
if len(current_steamlist) != len(new_streamlist):
|
|
340
|
+
return True
|
|
341
|
+
for x in range (0, len(current_steamlist)):
|
|
342
|
+
if current_steamlist[x].name != new_streamlist[x].name:
|
|
343
|
+
return True
|
|
344
|
+
if current_steamlist[x].hook != new_streamlist[x].hook:
|
|
345
|
+
return True
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == '__main__':
|
|
350
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
351
|
+
XYGraph = PlotWindow()
|
|
352
|
+
tools = SignalSpawner()
|
|
353
|
+
|
|
354
|
+
XYGraph.show()
|
|
355
|
+
tools.show()
|
|
356
|
+
|
|
357
|
+
sys.exit(app.exec())
|
|
358
|
+
|
|
359
|
+
# todo 1.1 - the plots shall be somehow contained within class. so I we could access their parameters like the data/colours/scales etc
|
|
360
|
+
# todo 1.1 - Add possibility to change the max X scale
|
|
361
|
+
# todo 1.1 - change the colour of a signal
|
|
362
|
+
# todo 1.1 - Add possibility to dynamically register multiple plots and show them on one graph
|