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.
Files changed (103) hide show
  1. {explorepy-4.3.0 → explorepy-4.5.0}/CHANGELOG.rst +11 -0
  2. {explorepy-4.3.0 → explorepy-4.5.0}/PKG-INFO +17 -16
  3. {explorepy-4.3.0 → explorepy-4.5.0}/README.rst +10 -9
  4. {explorepy-4.3.0 → explorepy-4.5.0}/docs/conf.py +1 -1
  5. {explorepy-4.3.0 → explorepy-4.5.0}/docs/usage.rst +4 -5
  6. {explorepy-4.3.0 → explorepy-4.5.0}/pyproject.toml +43 -7
  7. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/BLEClient.py +17 -1
  8. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/__init__.py +2 -2
  9. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/_exceptions.py +17 -0
  10. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/cli.py +1 -1
  11. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/command.py +45 -1
  12. explorepy-4.5.0/src/explorepy/csv_client.py +219 -0
  13. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/explore.py +44 -12
  14. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/filters.py +13 -1
  15. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/packet.py +94 -30
  16. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/parser.py +31 -10
  17. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/settings_manager.py +27 -44
  18. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/stream_processor.py +40 -14
  19. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/tools.py +73 -43
  20. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/PKG-INFO +17 -16
  21. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/SOURCES.txt +1 -1
  22. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/requires.txt +5 -5
  23. explorepy-4.3.0/.bumpversion.cfg +0 -26
  24. {explorepy-4.3.0 → explorepy-4.5.0}/.cookiecutterrc +0 -0
  25. {explorepy-4.3.0 → explorepy-4.5.0}/.coveragerc +0 -0
  26. {explorepy-4.3.0 → explorepy-4.5.0}/.editorconfig +0 -0
  27. {explorepy-4.3.0 → explorepy-4.5.0}/AUTHORS.rst +0 -0
  28. {explorepy-4.3.0 → explorepy-4.5.0}/CONTRIBUTING.rst +0 -0
  29. {explorepy-4.3.0 → explorepy-4.5.0}/LICENSE +0 -0
  30. {explorepy-4.3.0 → explorepy-4.5.0}/MANIFEST.in +0 -0
  31. {explorepy-4.3.0 → explorepy-4.5.0}/docs/authors.rst +0 -0
  32. {explorepy-4.3.0 → explorepy-4.5.0}/docs/changelog.rst +0 -0
  33. {explorepy-4.3.0 → explorepy-4.5.0}/docs/contributing.rst +0 -0
  34. {explorepy-4.3.0 → explorepy-4.5.0}/docs/explore_legacy_devices.rst +0 -0
  35. {explorepy-4.3.0 → explorepy-4.5.0}/docs/index.rst +0 -0
  36. {explorepy-4.3.0 → explorepy-4.5.0}/docs/installation.rst +0 -0
  37. {explorepy-4.3.0 → explorepy-4.5.0}/docs/logo.jpg +0 -0
  38. {explorepy-4.3.0 → explorepy-4.5.0}/docs/readme.rst +0 -0
  39. {explorepy-4.3.0 → explorepy-4.5.0}/docs/reference/explorepy.rst +0 -0
  40. {explorepy-4.3.0 → explorepy-4.5.0}/docs/reference/index.rst +0 -0
  41. {explorepy-4.3.0 → explorepy-4.5.0}/docs/requirements.txt +0 -0
  42. {explorepy-4.3.0 → explorepy-4.5.0}/docs/spelling_wordlist.txt +0 -0
  43. {explorepy-4.3.0 → explorepy-4.5.0}/setup.cfg +0 -0
  44. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/BTClient.py +0 -0
  45. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/bt_mock_client.py +0 -0
  46. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/bt_mock_server.py +0 -0
  47. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/debug.py +0 -0
  48. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/log_config.py +0 -0
  49. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy/serial_client.py +0 -0
  50. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/dependency_links.txt +0 -0
  51. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/entry_points.txt +0 -0
  52. {explorepy-4.3.0 → explorepy-4.5.0}/src/explorepy.egg-info/top_level.txt +0 -0
  53. {explorepy-4.3.0 → explorepy-4.5.0}/tests/README.md +0 -0
  54. {explorepy-4.3.0 → explorepy-4.5.0}/tests/__init__.py +0 -0
  55. {explorepy-4.3.0 → explorepy-4.5.0}/tests/conftest.py +0 -0
  56. {explorepy-4.3.0 → explorepy-4.5.0}/tests/integration_test_ble.py +0 -0
  57. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/calibration_info +0 -0
  58. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/calibration_info_usbc +0 -0
  59. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/cmd_rcv +0 -0
  60. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/cmd_stat +0 -0
  61. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info +0 -0
  62. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_ble +0 -0
  63. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_v2 +0 -0
  64. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/device_info_v2_2 +0 -0
  65. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/disconnect +0 -0
  66. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg16_ble +0 -0
  67. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg32 +0 -0
  68. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg94 +0 -0
  69. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98 +0 -0
  70. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_ble +0 -0
  71. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_usbc +0 -0
  72. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/eeg98_usbc_2 +0 -0
  73. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/env +0 -0
  74. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn +0 -0
  75. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn_2 +0 -0
  76. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/orn_matrix.txt +0 -0
  77. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/push_marker +0 -0
  78. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/in/trigger_in +0 -0
  79. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/axis_and_angle.txt +0 -0
  80. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/calibration_info_out.txt +0 -0
  81. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/calibration_info_usbc_out.txt +0 -0
  82. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/cmd_rcv_out.txt +0 -0
  83. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/cmd_stat_out.txt +0 -0
  84. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_ble_out.txt +0 -0
  85. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_out.txt +0 -0
  86. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_v2_2_out.txt +0 -0
  87. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/device_info_v2_out.txt +0 -0
  88. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/disconnect_out.txt +0 -0
  89. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg16_ble_out.txt +0 -0
  90. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg32_out.txt +0 -0
  91. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg94_out.txt +0 -0
  92. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_ble_out.txt +0 -0
  93. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_out.txt +0 -0
  94. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_out_fake.txt +0 -0
  95. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out.txt +0 -0
  96. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/eeg98_usbc_out_2.txt +0 -0
  97. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/env_out.txt +0 -0
  98. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/orn_2_out.txt +0 -0
  99. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/orn_out.txt +0 -0
  100. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/push_marker_out.txt +0 -0
  101. {explorepy-4.3.0 → explorepy-4.5.0}/tests/res/out/trigger_in_out.txt +0 -0
  102. {explorepy-4.3.0 → explorepy-4.5.0}/tests/test_packet.py +0 -0
  103. {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.0
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/markdown
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.38
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.3
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
- :scale: 100 %
48
- :align: left
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.3.0.svg
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.3.0...master
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
- ``explorepy`` overview
86
+ ExplorePy overview
88
87
  =========================
89
88
 
90
- ``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:
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 ``explorepy`` from PyPI run:
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 ``explorepy`` is running use:
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
- :scale: 100 %
3
- :align: left
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.3.0.svg
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.3.0...master
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
- ``explorepy`` overview
41
+ ExplorePy overview
43
42
  =========================
44
43
 
45
- ``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:
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 ``explorepy`` from PyPI run:
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 ``explorepy`` is running use:
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
 
@@ -28,7 +28,7 @@ project = 'explorepy'
28
28
  year = '2018-2025'
29
29
  author = 'Mentalab GmbH.'
30
30
  copyright = '{0}, {1}'.format(year, author)
31
- version = release = '4.3.0'
31
+ version = release = '4.5.0'
32
32
  pygments_style = 'trac'
33
33
  templates_path = ['.']
34
34
  extlinks = {
@@ -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 live monitoring
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
- To also record impedance data, use:
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
- 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. Add command arguments to overwrite files and set the duration of the recording (in seconds).
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.3.0"
7
+ version = "4.5.0"
8
8
  license = { text = "MIT" }
9
- readme = { file = "README.rst", content-type = "text/markdown" }
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.38',
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.3',
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
- time.sleep(1)
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.3.0'
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=50.0)
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