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.
Files changed (60) hide show
  1. pts_framework-0.6.1.dist-info/METADATA +95 -0
  2. pts_framework-0.6.1.dist-info/RECORD +60 -0
  3. pts_framework-0.6.1.dist-info/WHEEL +5 -0
  4. pts_framework-0.6.1.dist-info/entry_points.txt +3 -0
  5. pts_framework-0.6.1.dist-info/top_level.txt +1 -0
  6. pypts/XYGraph/StreamContainer.py +67 -0
  7. pypts/XYGraph/XY_graph.py +362 -0
  8. pypts/XYGraph/simulated_signals.py +88 -0
  9. pypts/YamVIEW/__init__.py +3 -0
  10. pypts/YamVIEW/customGUIModules.py +416 -0
  11. pypts/YamVIEW/recipe_creator.py +569 -0
  12. pypts/YamVIEW/recipe_sequencer_setup.py +523 -0
  13. pypts/YamVIEW/recipe_step_setup.py +577 -0
  14. pypts/YamVIEW/styles.py +13 -0
  15. pypts/YamVIEW/verify_recipe.py +105 -0
  16. pypts/__init__.py +41 -0
  17. pypts/__main__.py +43 -0
  18. pypts/_version.py +24 -0
  19. pypts/clim_chamber.py +41 -0
  20. pypts/common.py +11 -0
  21. pypts/dev_tests.py +82 -0
  22. pypts/event_proxy.py +226 -0
  23. pypts/example_tests.py +213 -0
  24. pypts/examples/environment_setup_tools/Minimal_setup/Minimal_setup_recipe.yml +212 -0
  25. pypts/examples/environment_setup_tools/Minimal_setup/README.md +41 -0
  26. pypts/examples/environment_setup_tools/Minimal_setup/init_env_min.py +46 -0
  27. pypts/examples/environment_setup_tools/Minimal_setup/tests/example_tests.py +86 -0
  28. pypts/examples/environment_setup_tools/Package_based_setup/Package_based_recipe.yml +215 -0
  29. pypts/examples/environment_setup_tools/Package_based_setup/README.md +55 -0
  30. pypts/examples/environment_setup_tools/Package_based_setup/init_env_pack.py +91 -0
  31. pypts/examples/environment_setup_tools/Package_based_setup/pyproject.toml +76 -0
  32. pypts/examples/environment_setup_tools/Package_based_setup/tests/example_tests.py +86 -0
  33. pypts/exceptions.py +3 -0
  34. pypts/gui.py +818 -0
  35. pypts/gui_components/__init__.py +5 -0
  36. pypts/gui_components/interaction_panel.py +206 -0
  37. pypts/gui_components/log_panel.py +53 -0
  38. pypts/gui_components/resources.py +87 -0
  39. pypts/gui_components/results_panel.py +162 -0
  40. pypts/gui_components/step_table.py +159 -0
  41. pypts/gui_components/styles.py +463 -0
  42. pypts/gui_components/toolbar.py +115 -0
  43. pypts/gui_theme.py +164 -0
  44. pypts/instruments/__init__.py +15 -0
  45. pypts/instruments/pendulum/__init__.py +7 -0
  46. pypts/instruments/pendulum/cnt91.py +122 -0
  47. pypts/pts.py +250 -0
  48. pypts/recipe.py +802 -0
  49. pypts/recipe_artifacts.py +85 -0
  50. pypts/recipe_language.py +389 -0
  51. pypts/recipe_parser.py +552 -0
  52. pypts/recipe_reference.py +217 -0
  53. pypts/recipes/shared_object_example.py +23 -0
  54. pypts/recipes/shared_resource_client.py +24 -0
  55. pypts/recipes/shared_resource_server.py +62 -0
  56. pypts/recipes/shared_resource_tests_example.py +31 -0
  57. pypts/report.py +544 -0
  58. pypts/startup.py +86 -0
  59. pypts/steps.py +1257 -0
  60. 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
+ ![pypts quick run](images/pypts_quick_run.gif)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ init_env_min = pypts.examples.environment_setup_tools.Minimal_setup.init_env_min:main
3
+ init_env_pack = pypts.examples.environment_setup_tools.Package_based_setup.init_env_pack:main
@@ -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