explorepy 4.3.1__tar.gz → 4.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {explorepy-4.3.1 → explorepy-4.5.0}/CHANGELOG.rst +5 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/PKG-INFO +17 -16
- {explorepy-4.3.1 → explorepy-4.5.0}/README.rst +10 -9
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/conf.py +1 -1
- {explorepy-4.3.1 → explorepy-4.5.0}/pyproject.toml +43 -7
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/BLEClient.py +17 -1
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/__init__.py +2 -2
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/_exceptions.py +17 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/command.py +45 -1
- explorepy-4.5.0/src/explorepy/csv_client.py +219 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/explore.py +27 -11
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/filters.py +13 -1
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/packet.py +94 -30
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/parser.py +26 -3
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/settings_manager.py +27 -44
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/stream_processor.py +40 -14
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/tools.py +70 -42
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/PKG-INFO +17 -16
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/SOURCES.txt +1 -1
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/requires.txt +5 -5
- explorepy-4.3.1/.bumpversion.cfg +0 -26
- {explorepy-4.3.1 → explorepy-4.5.0}/.cookiecutterrc +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/.coveragerc +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/.editorconfig +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/AUTHORS.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/CONTRIBUTING.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/LICENSE +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/MANIFEST.in +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/authors.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/changelog.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/contributing.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/explore_legacy_devices.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/index.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/installation.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/logo.jpg +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/readme.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/reference/explorepy.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/reference/index.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/requirements.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/spelling_wordlist.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/docs/usage.rst +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/setup.cfg +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/BTClient.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/bt_mock_client.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/bt_mock_server.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/cli.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/debug.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/log_config.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy/serial_client.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/dependency_links.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/entry_points.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/src/explorepy.egg-info/top_level.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/README.md +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/__init__.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/conftest.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/integration_test_ble.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/calibration_info +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/calibration_info_usbc +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/cmd_rcv +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/cmd_stat +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/device_info +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/device_info_ble +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/device_info_v2 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/device_info_v2_2 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/disconnect +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg16_ble +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg32 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg94 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg98 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg98_ble +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg98_usbc +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/eeg98_usbc_2 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/env +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/orn +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/orn_2 +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/orn_matrix.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/push_marker +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/in/trigger_in +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/axis_and_angle.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/calibration_info_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/calibration_info_usbc_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/cmd_rcv_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/cmd_stat_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/device_info_ble_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/device_info_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/device_info_v2_2_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/device_info_v2_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/disconnect_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg16_ble_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg32_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg94_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg98_ble_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg98_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg98_out_fake.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out_2.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/env_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/orn_2_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/orn_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/push_marker_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/res/out/trigger_in_out.txt +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tests/test_packet.py +0 -0
- {explorepy-4.3.1 → explorepy-4.5.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: explorepy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.0
|
|
4
4
|
Author-email: MentaLab Hub <support@mentab.org>
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/Mentalab-hub/explorepy
|
|
@@ -18,14 +18,14 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
18
18
|
Classifier: Operating System :: POSIX :: Linux
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Description-Content-Type: text/
|
|
21
|
+
Description-Content-Type: text/x-rst
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
License-File: AUTHORS.rst
|
|
24
|
-
Requires-Dist: numpy
|
|
25
|
-
Requires-Dist: scipy
|
|
26
|
-
Requires-Dist: pyEDFlib==0.1.
|
|
24
|
+
Requires-Dist: numpy==2.1.3
|
|
25
|
+
Requires-Dist: scipy==1.17.1
|
|
26
|
+
Requires-Dist: pyEDFlib==0.1.42
|
|
27
27
|
Requires-Dist: click==7.1.2
|
|
28
|
-
Requires-Dist: appdirs==1.4.
|
|
28
|
+
Requires-Dist: appdirs==1.4.4
|
|
29
29
|
Requires-Dist: sentry_sdk==2.8.0
|
|
30
30
|
Requires-Dist: mne
|
|
31
31
|
Requires-Dist: eeglabio
|
|
@@ -33,7 +33,7 @@ Requires-Dist: pandas
|
|
|
33
33
|
Requires-Dist: pyserial
|
|
34
34
|
Requires-Dist: pyyaml
|
|
35
35
|
Requires-Dist: bleak==0.22.3
|
|
36
|
-
Requires-Dist: pylsl
|
|
36
|
+
Requires-Dist: pylsl==1.18.2
|
|
37
37
|
Requires-Dist: numba
|
|
38
38
|
Provides-Extra: test
|
|
39
39
|
Requires-Dist: pytest==6.2.5; extra == "test"
|
|
@@ -44,9 +44,8 @@ Requires-Dist: isort==5.10.1; extra == "test"
|
|
|
44
44
|
Dynamic: license-file
|
|
45
45
|
|
|
46
46
|
.. image:: https://raw.githubusercontent.com/Mentalab-hub/explorepy/master/docs/logo.jpg
|
|
47
|
-
:
|
|
48
|
-
:
|
|
49
|
-
|
|
47
|
+
:alt: Explorepy
|
|
48
|
+
:target: https://github.com/Mentalab-hub/explorepy
|
|
50
49
|
|
|
51
50
|
.. start-badges
|
|
52
51
|
|
|
@@ -62,9 +61,9 @@ Dynamic: license-file
|
|
|
62
61
|
:target: https://pypi.org/project/explorepy
|
|
63
62
|
|
|
64
63
|
|
|
65
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.
|
|
64
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.5.0.svg
|
|
66
65
|
:alt: Commits since latest release
|
|
67
|
-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.
|
|
66
|
+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.5.0...master
|
|
68
67
|
|
|
69
68
|
|
|
70
69
|
.. |wheel| image:: https://img.shields.io/pypi/wheel/explorepy.svg
|
|
@@ -84,10 +83,10 @@ Dynamic: license-file
|
|
|
84
83
|
|
|
85
84
|
|
|
86
85
|
=========================
|
|
87
|
-
|
|
86
|
+
ExplorePy overview
|
|
88
87
|
=========================
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
ExplorePy is an open-source Python API designed to collect and process ExG data using Mentalab's Explore device. Amongst other things, ExplorePy provides the following features:
|
|
91
90
|
|
|
92
91
|
* Real-time streaming of ExG, orientation and environmental data.
|
|
93
92
|
* Data recording in CSV and BDF+ formats.
|
|
@@ -106,7 +105,7 @@ Requirements
|
|
|
106
105
|
|
|
107
106
|
Detailed installation instructions can be found on the `installation page <https://explorepy.readthedocs.io/en/latest/installation.html>`_.
|
|
108
107
|
|
|
109
|
-
To install
|
|
108
|
+
To install ExplorePy from PyPI run:
|
|
110
109
|
::
|
|
111
110
|
|
|
112
111
|
pip install explorepy
|
|
@@ -123,12 +122,14 @@ Get started
|
|
|
123
122
|
|
|
124
123
|
CLI command
|
|
125
124
|
-----------
|
|
126
|
-
To check
|
|
125
|
+
To check ExplorePy is running use:
|
|
127
126
|
::
|
|
127
|
+
|
|
128
128
|
explorepy acquire -n Explore_XXXX
|
|
129
129
|
|
|
130
130
|
For help, use:
|
|
131
131
|
::
|
|
132
|
+
|
|
132
133
|
explorepy -h
|
|
133
134
|
|
|
134
135
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
.. image:: https://raw.githubusercontent.com/Mentalab-hub/explorepy/master/docs/logo.jpg
|
|
2
|
-
:
|
|
3
|
-
:
|
|
4
|
-
|
|
2
|
+
:alt: Explorepy
|
|
3
|
+
:target: https://github.com/Mentalab-hub/explorepy
|
|
5
4
|
|
|
6
5
|
.. start-badges
|
|
7
6
|
|
|
@@ -17,9 +16,9 @@
|
|
|
17
16
|
:target: https://pypi.org/project/explorepy
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.
|
|
19
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.5.0.svg
|
|
21
20
|
:alt: Commits since latest release
|
|
22
|
-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.
|
|
21
|
+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.5.0...master
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
.. |wheel| image:: https://img.shields.io/pypi/wheel/explorepy.svg
|
|
@@ -39,10 +38,10 @@
|
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
=========================
|
|
42
|
-
|
|
41
|
+
ExplorePy overview
|
|
43
42
|
=========================
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
ExplorePy is an open-source Python API designed to collect and process ExG data using Mentalab's Explore device. Amongst other things, ExplorePy provides the following features:
|
|
46
45
|
|
|
47
46
|
* Real-time streaming of ExG, orientation and environmental data.
|
|
48
47
|
* Data recording in CSV and BDF+ formats.
|
|
@@ -61,7 +60,7 @@ Requirements
|
|
|
61
60
|
|
|
62
61
|
Detailed installation instructions can be found on the `installation page <https://explorepy.readthedocs.io/en/latest/installation.html>`_.
|
|
63
62
|
|
|
64
|
-
To install
|
|
63
|
+
To install ExplorePy from PyPI run:
|
|
65
64
|
::
|
|
66
65
|
|
|
67
66
|
pip install explorepy
|
|
@@ -78,12 +77,14 @@ Get started
|
|
|
78
77
|
|
|
79
78
|
CLI command
|
|
80
79
|
-----------
|
|
81
|
-
To check
|
|
80
|
+
To check ExplorePy is running use:
|
|
82
81
|
::
|
|
82
|
+
|
|
83
83
|
explorepy acquire -n Explore_XXXX
|
|
84
84
|
|
|
85
85
|
For help, use:
|
|
86
86
|
::
|
|
87
|
+
|
|
87
88
|
explorepy -h
|
|
88
89
|
|
|
89
90
|
|
|
@@ -4,9 +4,9 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'explorepy'
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.5.0"
|
|
8
8
|
license = { text = "MIT" }
|
|
9
|
-
readme = { file = "README.rst", content-type = "text/
|
|
9
|
+
readme = { file = "README.rst", content-type = "text/x-rst"}
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "MentaLab Hub", email = "support@mentab.org" },
|
|
12
12
|
]
|
|
@@ -29,11 +29,11 @@ classifiers = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
32
|
-
'numpy',
|
|
33
|
-
'scipy',
|
|
34
|
-
'pyEDFlib==0.1.
|
|
32
|
+
'numpy==2.1.3',
|
|
33
|
+
'scipy==1.17.1',
|
|
34
|
+
'pyEDFlib==0.1.42',
|
|
35
35
|
'click==7.1.2',
|
|
36
|
-
'appdirs==1.4.
|
|
36
|
+
'appdirs==1.4.4',
|
|
37
37
|
'sentry_sdk==2.8.0',
|
|
38
38
|
'mne',
|
|
39
39
|
'eeglabio',
|
|
@@ -41,7 +41,7 @@ dependencies = [
|
|
|
41
41
|
'pyserial',
|
|
42
42
|
'pyyaml',
|
|
43
43
|
'bleak==0.22.3',
|
|
44
|
-
'pylsl',
|
|
44
|
+
'pylsl==1.18.2',
|
|
45
45
|
'numba']
|
|
46
46
|
|
|
47
47
|
[tool.setuptools]
|
|
@@ -60,3 +60,39 @@ test = [
|
|
|
60
60
|
|
|
61
61
|
[project.scripts]
|
|
62
62
|
explorepy = "explorepy.cli:cli"
|
|
63
|
+
|
|
64
|
+
# bumpoversion config
|
|
65
|
+
[tool.bumpversion]
|
|
66
|
+
current_version = "4.5.0"
|
|
67
|
+
commit = false
|
|
68
|
+
tag = false
|
|
69
|
+
|
|
70
|
+
[[tool.bumpversion.files]]
|
|
71
|
+
filename = "pyproject.toml"
|
|
72
|
+
search = 'version = "{current_version}"'
|
|
73
|
+
replace = 'version = "{new_version}"'
|
|
74
|
+
|
|
75
|
+
[[tool.bumpversion.files]]
|
|
76
|
+
filename = "README.rst"
|
|
77
|
+
search = "v{current_version}."
|
|
78
|
+
replace = "v{new_version}."
|
|
79
|
+
|
|
80
|
+
[[tool.bumpversion.files]]
|
|
81
|
+
filename = "docs/conf.py"
|
|
82
|
+
search = "version = release = '{current_version}'"
|
|
83
|
+
replace = "version = release = '{new_version}'"
|
|
84
|
+
|
|
85
|
+
[[tool.bumpversion.files]]
|
|
86
|
+
filename = "src/explorepy/__init__.py"
|
|
87
|
+
search = "__version__ = '{current_version}'"
|
|
88
|
+
replace = "__version__ = '{new_version}'"
|
|
89
|
+
|
|
90
|
+
[[tool.bumpversion.files]]
|
|
91
|
+
filename = "installer/windows/installer.cfg"
|
|
92
|
+
search = "explorepy=={current_version}"
|
|
93
|
+
replace = "explorepy=={new_version}"
|
|
94
|
+
|
|
95
|
+
[[tool.bumpversion.files]]
|
|
96
|
+
filename = "installer/windows/installer.cfg"
|
|
97
|
+
search = "version={current_version}"
|
|
98
|
+
replace = "version={new_version}"
|
|
@@ -17,6 +17,7 @@ from bleak import (
|
|
|
17
17
|
|
|
18
18
|
from explorepy._exceptions import (
|
|
19
19
|
BleDisconnectionError,
|
|
20
|
+
BleDisconnectionFailedError,
|
|
20
21
|
DeviceNotFoundError,
|
|
21
22
|
UnexpectedConnectionError
|
|
22
23
|
)
|
|
@@ -200,7 +201,22 @@ class BLEClient(BTClient):
|
|
|
200
201
|
if self.notify_task:
|
|
201
202
|
self.notify_task.cancel()
|
|
202
203
|
self.read_event.set()
|
|
203
|
-
|
|
204
|
+
|
|
205
|
+
min_time_to_wait = 0.5
|
|
206
|
+
max_time_to_wait = 5.
|
|
207
|
+
wait_start = time.time()
|
|
208
|
+
time_passed = 0.
|
|
209
|
+
if self.client is not None:
|
|
210
|
+
while self.client.is_connected:
|
|
211
|
+
time.sleep(0.1)
|
|
212
|
+
time_passed = time.time() - wait_start
|
|
213
|
+
if time_passed >= max_time_to_wait:
|
|
214
|
+
raise BleDisconnectionFailedError(f"Bleak client still not reporting disconnected after waiting "
|
|
215
|
+
f"{max_time_to_wait}.")
|
|
216
|
+
if time_passed < min_time_to_wait:
|
|
217
|
+
# Artificial delay to make the user think things are happening :)
|
|
218
|
+
time.sleep(min_time_to_wait - time_passed)
|
|
219
|
+
|
|
204
220
|
self.stop_read_loop()
|
|
205
221
|
self.ble_device = None
|
|
206
222
|
self.buffer = Queue()
|
|
@@ -20,11 +20,11 @@ from .explore import Explore # noqa
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
__all__ = ["Explore", "command", "tools", "log_config"]
|
|
23
|
-
__version__ = '4.
|
|
23
|
+
__version__ = '4.5.0'
|
|
24
24
|
|
|
25
25
|
this = sys.modules[__name__]
|
|
26
26
|
# TODO appropriate library
|
|
27
|
-
bt_interface_list = ['sdk', 'ble', 'mock', 'pyserial', 'usb']
|
|
27
|
+
bt_interface_list = ['sdk', 'ble', 'mock', 'pyserial', 'usb', 'csv']
|
|
28
28
|
this._bt_interface = 'ble'
|
|
29
29
|
|
|
30
30
|
if not sys.version_info >= (3, 6):
|
|
@@ -55,6 +55,23 @@ class BleDisconnectionError(Exception):
|
|
|
55
55
|
pass
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class BleDisconnectionFailedError(Exception):
|
|
59
|
+
"""
|
|
60
|
+
Exception for client fails to achieve disconnected state
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ImpedanceModeActiveError(Exception):
|
|
66
|
+
"""
|
|
67
|
+
Exception for ASR, raised when impedance is running
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, message="ASR can not run because impedance mode is active.\n"
|
|
71
|
+
"Please disable impedance and try again\n"):
|
|
72
|
+
super().__init__(message)
|
|
73
|
+
|
|
74
|
+
|
|
58
75
|
class ExplorePyDeprecationError(Exception):
|
|
59
76
|
def __init__(self, message="Explorepy support for legacy devices is deprecated.\n"
|
|
60
77
|
"Please install explorepy 3.2.1 from Github or use the following command from Anaconda "
|
|
@@ -16,6 +16,7 @@ class CommandID(Enum):
|
|
|
16
16
|
"""Command ID enum class"""
|
|
17
17
|
API2BCMD = b'\xA0'
|
|
18
18
|
API4BCMD = b'\xB0'
|
|
19
|
+
API64BCMD = b'\xC0'
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class OpcodeID(Enum):
|
|
@@ -27,6 +28,7 @@ class OpcodeID(Enum):
|
|
|
27
28
|
CMD_ZM_ENABLE = b'\xA7'
|
|
28
29
|
CMD_SOFT_RESET = b'\xA8'
|
|
29
30
|
CMD_TEST_SIG = b'\xAA'
|
|
31
|
+
CMD_SET_EMMC_TIME = b'\xAB'
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class DeviceConfiguration:
|
|
@@ -182,6 +184,19 @@ class Command4B(Command):
|
|
|
182
184
|
"""prints the appropriate info about the command. """
|
|
183
185
|
|
|
184
186
|
|
|
187
|
+
class Command64B(Command):
|
|
188
|
+
"""An abstract base class for Explore 40 Byte command data length packets"""
|
|
189
|
+
|
|
190
|
+
def __init__(self):
|
|
191
|
+
super().__init__()
|
|
192
|
+
self.pid = CommandID.API64BCMD
|
|
193
|
+
self.payload_length = int2bytearray(40, 2)
|
|
194
|
+
|
|
195
|
+
@abc.abstractmethod
|
|
196
|
+
def __str__(self):
|
|
197
|
+
"""prints the appropriate info about the command. """
|
|
198
|
+
|
|
199
|
+
|
|
185
200
|
class SetSPS(Command2B):
|
|
186
201
|
"""Set the sampling rate of ExG device"""
|
|
187
202
|
|
|
@@ -213,6 +228,34 @@ class SetSPS(Command2B):
|
|
|
213
228
|
return "Set sampling rate command"
|
|
214
229
|
|
|
215
230
|
|
|
231
|
+
class SetBinaryTime(Command64B):
|
|
232
|
+
def __init__(self):
|
|
233
|
+
super().__init__()
|
|
234
|
+
self.opcode = OpcodeID.CMD_SET_EMMC_TIME
|
|
235
|
+
from datetime import datetime
|
|
236
|
+
now = datetime.now()
|
|
237
|
+
if not (2020 < now.year < 2099):
|
|
238
|
+
raise ValueError(f"Unsupported year: {now.year}")
|
|
239
|
+
date_time = bytes(
|
|
240
|
+
[
|
|
241
|
+
now.year - 2000,
|
|
242
|
+
now.month,
|
|
243
|
+
now.day,
|
|
244
|
+
now.weekday(),
|
|
245
|
+
now.hour,
|
|
246
|
+
now.minute,
|
|
247
|
+
now.second,
|
|
248
|
+
1,
|
|
249
|
+
2,
|
|
250
|
+
3,
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
self.param = date_time + bytes(41)
|
|
254
|
+
|
|
255
|
+
def __str__(self):
|
|
256
|
+
return "set time cmd"
|
|
257
|
+
|
|
258
|
+
|
|
216
259
|
class MemoryFormat(Command2B):
|
|
217
260
|
"""Format device memory"""
|
|
218
261
|
|
|
@@ -282,7 +325,8 @@ class SetChTest(Command2B):
|
|
|
282
325
|
|
|
283
326
|
COMMAND_CLASS_DICT = {
|
|
284
327
|
CommandID.API2BCMD: Command2B,
|
|
285
|
-
CommandID.API4BCMD: Command4B
|
|
328
|
+
CommandID.API4BCMD: Command4B,
|
|
329
|
+
CommandID.API64BCMD: Command64B
|
|
286
330
|
}
|
|
287
331
|
|
|
288
332
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from enum import (
|
|
4
|
+
Enum,
|
|
5
|
+
auto
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from pylsl import local_clock
|
|
10
|
+
|
|
11
|
+
from explorepy.packet import (
|
|
12
|
+
BleImpedancePacket,
|
|
13
|
+
DeviceInfoBLE,
|
|
14
|
+
OrientationV2
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientState(Enum):
|
|
19
|
+
DISCONNECTED = auto()
|
|
20
|
+
CONNECTED = auto()
|
|
21
|
+
STREAMING = auto()
|
|
22
|
+
STOPPED = auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PacketSize(Enum):
|
|
26
|
+
EEG_8 = 40
|
|
27
|
+
EEG_32 = 112
|
|
28
|
+
ORN = 50
|
|
29
|
+
DEVICE_INFO = 38
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CsvClient:
|
|
33
|
+
def __init__(self, channel_count, file_path: str = None):
|
|
34
|
+
if file_path is None:
|
|
35
|
+
self.file_name = "32channel_semidry_artefacts_ExG.csv"
|
|
36
|
+
file_path = os.path.join("/Users/sonjastefani/Documents/dev/explore-desktop/test-data/", self.file_name)
|
|
37
|
+
else:
|
|
38
|
+
self.file_name = os.path.split(file_path)[-1]
|
|
39
|
+
|
|
40
|
+
if not os.path.isfile(file_path):
|
|
41
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
42
|
+
|
|
43
|
+
self.server = CsvServer(
|
|
44
|
+
channel_count=channel_count,
|
|
45
|
+
csv_path=file_path,
|
|
46
|
+
loop=True
|
|
47
|
+
)
|
|
48
|
+
self._state = ClientState.DISCONNECTED
|
|
49
|
+
|
|
50
|
+
def set_state(self, state: ClientState):
|
|
51
|
+
if not isinstance(state, ClientState):
|
|
52
|
+
raise ValueError("Invalid client state")
|
|
53
|
+
self._state = state
|
|
54
|
+
|
|
55
|
+
def get_state(self) -> ClientState:
|
|
56
|
+
return self._state
|
|
57
|
+
|
|
58
|
+
def connect(self):
|
|
59
|
+
if self._state != ClientState.DISCONNECTED:
|
|
60
|
+
return False
|
|
61
|
+
self.set_state(ClientState.CONNECTED)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def disconnect(self):
|
|
65
|
+
self.set_state(ClientState.DISCONNECTED)
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def start_streaming(self):
|
|
69
|
+
if self._state != ClientState.CONNECTED:
|
|
70
|
+
raise RuntimeError(f"Cannot start streaming from state {self._state}")
|
|
71
|
+
self.set_state(ClientState.STREAMING)
|
|
72
|
+
|
|
73
|
+
def stop_streaming(self):
|
|
74
|
+
if self._state == ClientState.STREAMING:
|
|
75
|
+
self.set_state(ClientState.STOPPED)
|
|
76
|
+
|
|
77
|
+
def read(self):
|
|
78
|
+
if self._state != ClientState.STREAMING:
|
|
79
|
+
if self._state == ClientState.CONNECTED:
|
|
80
|
+
self.set_state(ClientState.STREAMING)
|
|
81
|
+
device_info_packet = DeviceInfoMock(timestamp=self.server.ts, payload=None)
|
|
82
|
+
device_info_packet.packet_size = PacketSize.DEVICE_INFO
|
|
83
|
+
device_info_packet.set_info(self.server.device_info)
|
|
84
|
+
return device_info_packet
|
|
85
|
+
|
|
86
|
+
self.server.tick += 1
|
|
87
|
+
if self.server.tick % 5 == 0:
|
|
88
|
+
orn_packet = OrientationMock(timestamp=self.server.ts, payload=None)
|
|
89
|
+
orn_packet.set_data(self.server.orn_value)
|
|
90
|
+
orn_packet.packet_size = PacketSize.ORN
|
|
91
|
+
return orn_packet
|
|
92
|
+
|
|
93
|
+
self.server.ts += self.server.time_period
|
|
94
|
+
sleep_time = self.server.ts - local_clock()
|
|
95
|
+
if sleep_time > 0:
|
|
96
|
+
time.sleep(sleep_time)
|
|
97
|
+
eeg_packet = BleImpedancePacket(timestamp=self.server.ts, payload=None)
|
|
98
|
+
try:
|
|
99
|
+
eeg_packet.data, eeg_packet.timestamp = self.server.read_sample()
|
|
100
|
+
except StopIteration:
|
|
101
|
+
self.set_state(ClientState.STOPPED)
|
|
102
|
+
return None
|
|
103
|
+
eeg_packet.packet_size = self.server.packet_size
|
|
104
|
+
return eeg_packet
|
|
105
|
+
|
|
106
|
+
def write(self, bytes):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CsvServer:
|
|
111
|
+
def __init__(self, channel_count: int, csv_path: str, loop: bool = True):
|
|
112
|
+
if channel_count not in (8, 32):
|
|
113
|
+
raise ValueError("Only 8 or 32 channels supported")
|
|
114
|
+
|
|
115
|
+
self.channel_count = channel_count
|
|
116
|
+
self.loop = loop
|
|
117
|
+
self.csv_data = np.loadtxt(csv_path, delimiter=',', skiprows=1) # skip row 0
|
|
118
|
+
|
|
119
|
+
self.csv_ts = self.csv_data[:, 0]
|
|
120
|
+
self.csv_data = self.csv_data[:, 1:]
|
|
121
|
+
|
|
122
|
+
if self.csv_data.shape[1] != channel_count:
|
|
123
|
+
print('######################', self.csv_data.shape)
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"CSV has {self.csv_data.shape[1]} columns, "
|
|
126
|
+
f"expected {channel_count}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.row_idx = 0
|
|
130
|
+
self.num_rows = self.csv_data.shape[0]
|
|
131
|
+
self.device_info_ble_32ch = {
|
|
132
|
+
'device_name': 'Explore_DABD',
|
|
133
|
+
'firmware_version': '9.6.9',
|
|
134
|
+
'adc_mask': [1] * 8,
|
|
135
|
+
'sampling_rate': 250,
|
|
136
|
+
'is_imp_mode': False,
|
|
137
|
+
'board_id': 'PCB_304_801p2_X',
|
|
138
|
+
'memory_info': 1,
|
|
139
|
+
'max_online_sps': 250,
|
|
140
|
+
'max_offline_sps': 2000
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
self.device_info_ble_8ch = {
|
|
144
|
+
'device_name': 'Explore_AAAQ',
|
|
145
|
+
'firmware_version': '7.6.9',
|
|
146
|
+
'adc_mask': [1] * 8,
|
|
147
|
+
'sampling_rate': 250,
|
|
148
|
+
'is_imp_mode': False,
|
|
149
|
+
'board_id': 'PCB_303_801E_XX',
|
|
150
|
+
'memory_info': 1,
|
|
151
|
+
'max_online_sps': 1000,
|
|
152
|
+
'max_offline_sps': 8000
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
self.device_info = (
|
|
156
|
+
self.device_info_ble_8ch
|
|
157
|
+
if channel_count == 8
|
|
158
|
+
else self.device_info_ble_32ch
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self.fs = self.device_info['sampling_rate']
|
|
162
|
+
self.time_period = np.round(1 / self.fs, 3)
|
|
163
|
+
self.ts = local_clock()
|
|
164
|
+
self.tick = 0
|
|
165
|
+
|
|
166
|
+
self.packet_size = (
|
|
167
|
+
PacketSize.EEG_8 if channel_count == 8 else PacketSize.EEG_32
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self.orn_value = [
|
|
171
|
+
5.002, -3.904, 1001.01, 420.0, -70.0, 210.0,
|
|
172
|
+
103.36, 804.08, -532.0, -0.0023,
|
|
173
|
+
-0.0028, 0.0371, 0.9993
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
def read_sample(self):
|
|
177
|
+
if self.row_idx >= self.num_rows:
|
|
178
|
+
if not self.loop:
|
|
179
|
+
raise StopIteration("End of CSV reached")
|
|
180
|
+
self.row_idx = 0
|
|
181
|
+
|
|
182
|
+
ts = self.csv_ts[self.row_idx]
|
|
183
|
+
sample = self.csv_data[self.row_idx]
|
|
184
|
+
self.row_idx += 1
|
|
185
|
+
|
|
186
|
+
return sample.reshape(self.channel_count, 1), ts
|
|
187
|
+
|
|
188
|
+
def read_device_info(self):
|
|
189
|
+
return self.device_info
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class DeviceInfoMock(DeviceInfoBLE):
|
|
193
|
+
def __init__(self, timestamp, payload, time_offset=0):
|
|
194
|
+
self.timestamp = timestamp
|
|
195
|
+
|
|
196
|
+
def _convert(self, bin_data):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
def set_info(self, info):
|
|
200
|
+
self.info = info
|
|
201
|
+
|
|
202
|
+
def get_info(self):
|
|
203
|
+
return self.info
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class OrientationMock(OrientationV2):
|
|
207
|
+
"""Orientation data packet"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, timestamp, payload, time_offset=0):
|
|
210
|
+
self.timestamp = timestamp
|
|
211
|
+
|
|
212
|
+
def _convert(self, bin_data):
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def get_data(self, srate=None):
|
|
216
|
+
return [self.timestamp], self.data
|
|
217
|
+
|
|
218
|
+
def set_data(self, data):
|
|
219
|
+
self.data = data
|