explorepy 4.3.0__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.0 → explorepy-4.5.0}/CHANGELOG.rst +11 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/PKG-INFO +17 -16
- {explorepy-4.3.0 → explorepy-4.5.0}/README.rst +10 -9
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/conf.py +1 -1
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/usage.rst +4 -5
- {explorepy-4.3.0 → explorepy-4.5.0}/pyproject.toml +43 -7
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/BLEClient.py +17 -1
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/__init__.py +2 -2
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/_exceptions.py +17 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/cli.py +1 -1
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/command.py +45 -1
- explorepy-4.5.0/src/explorepy/csv_client.py +219 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/explore.py +44 -12
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/filters.py +13 -1
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/packet.py +94 -30
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/parser.py +31 -10
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/settings_manager.py +27 -44
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/stream_processor.py +40 -14
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/tools.py +73 -43
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/PKG-INFO +17 -16
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/SOURCES.txt +1 -1
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/requires.txt +5 -5
- explorepy-4.3.0/.bumpversion.cfg +0 -26
- {explorepy-4.3.0 → explorepy-4.5.0}/.cookiecutterrc +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/.coveragerc +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/.editorconfig +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/AUTHORS.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/CONTRIBUTING.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/LICENSE +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/MANIFEST.in +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/authors.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/changelog.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/contributing.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/explore_legacy_devices.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/index.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/installation.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/logo.jpg +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/readme.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/reference/explorepy.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/reference/index.rst +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/requirements.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/docs/spelling_wordlist.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/setup.cfg +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/BTClient.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/bt_mock_client.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/bt_mock_server.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/debug.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/log_config.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/serial_client.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/dependency_links.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/entry_points.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/top_level.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/README.md +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/__init__.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/conftest.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/integration_test_ble.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/calibration_info +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/calibration_info_usbc +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/cmd_rcv +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/cmd_stat +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_ble +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_v2 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_v2_2 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/disconnect +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg16_ble +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg32 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg94 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_ble +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_usbc +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_usbc_2 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/env +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn_2 +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn_matrix.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/push_marker +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/trigger_in +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/axis_and_angle.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/calibration_info_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/calibration_info_usbc_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/cmd_rcv_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/cmd_stat_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_ble_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_v2_2_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_v2_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/disconnect_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg16_ble_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg32_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg94_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_ble_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_out_fake.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out_2.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/env_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/orn_2_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/orn_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/push_marker_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/trigger_in_out.txt +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tests/test_packet.py +0 -0
- {explorepy-4.3.0 → explorepy-4.5.0}/tox.ini +0 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
|
+
4.5.0 (5.6.2025)
|
|
5
|
+
------------------
|
|
6
|
+
* Live impedance
|
|
7
|
+
* Allow pushing preprocessed data to lab streaming layer
|
|
8
|
+
* Support new FW version
|
|
9
|
+
|
|
10
|
+
4.3.1 (17.9.2025)
|
|
11
|
+
------------------
|
|
12
|
+
* Bugfix for 32 channel impedance recording
|
|
13
|
+
* Bugfix for BLE connection drop
|
|
14
|
+
* Update binary converter
|
|
4
15
|
|
|
5
16
|
4.3.0 (10.9.2025)
|
|
6
17
|
------------------
|
|
@@ -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
|
|
|
@@ -55,7 +55,7 @@ Connects to a device and records ExG and orientation data into two separate file
|
|
|
55
55
|
-d, --duration <integer> Recording duration in seconds
|
|
56
56
|
--edf Write in EDF file
|
|
57
57
|
--csv Write in csv file (default type)
|
|
58
|
-
--imp-mode Enable impedance mode with
|
|
58
|
+
--imp-mode Enable impedance mode with real-time monitoring (CSV only)
|
|
59
59
|
-h, --help Show this message and exit.
|
|
60
60
|
|
|
61
61
|
|
|
@@ -222,14 +222,13 @@ Recording
|
|
|
222
222
|
You can record data in realtime to EDF (BDF+) or CSV files using:
|
|
223
223
|
::
|
|
224
224
|
explore.record_data(file_name='test', duration=120, file_type='csv')
|
|
225
|
+
This will record data in three separate files: "``test_ExG.csv``", "``test_ORN.csv``" and "``test_marker.csv``", which contain ExG data, orientation data (accelerometer, gyroscope, magnetometer) and event markers respectively. It is also possible to add arguments to overwrite files.
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
Enable imp_mode argument to record impedance data as well:
|
|
227
228
|
::
|
|
228
229
|
explore.record_data(file_name='test', duration=120, file_type='csv', imp_mode=True)
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
::
|
|
232
|
-
explore.record_data(file_name='test', do_overwrite=True, file_type='csv', duration=120)
|
|
231
|
+
.. note:: Impedance recording is only supported for CSV files!
|
|
233
232
|
|
|
234
233
|
.. note:: To load EDF files, you can use `pyedflib <https://github.com/holgern/pyedflib>`_ or `mne <https://github.com/mne-tools/mne-python>`_ (for mne, you may need to change the file extension to ``bdf`` manually) in Python.
|
|
235
234
|
|
|
@@ -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 "
|
|
@@ -76,7 +76,7 @@ def acquire(name, address, duration):
|
|
|
76
76
|
@click.option("--edf", 'file_type', flag_value='edf', help="Write in EDF file")
|
|
77
77
|
@click.option("--csv", 'file_type', flag_value='csv', help="Write in csv file (default type)", default=True)
|
|
78
78
|
@click.option("--imp-mode", is_flag=True, help="Enable impedance mode with live monitoring")
|
|
79
|
-
@click.option("-nf", "--notch-freq", help="Notch frequency for impedance mode initialization", type=float, default=
|
|
79
|
+
@click.option("-nf", "--notch-freq", help="Notch frequency for impedance mode initialization", type=float, default=None)
|
|
80
80
|
@verify_inputs
|
|
81
81
|
def record_data(address, name, filename, overwrite, duration, file_type, imp_mode, notch_freq):
|
|
82
82
|
"""Record data from Explore to a file"""
|
|
@@ -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
|