explorepy 4.2.0__tar.gz → 4.3.1__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.2.0 → explorepy-4.3.1}/.bumpversion.cfg +1 -1
- {explorepy-4.2.0 → explorepy-4.3.1}/CHANGELOG.rst +13 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/PKG-INFO +3 -3
- {explorepy-4.2.0 → explorepy-4.3.1}/README.rst +2 -2
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/conf.py +1 -1
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/usage.rst +89 -19
- {explorepy-4.2.0 → explorepy-4.3.1}/pyproject.toml +1 -1
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/BLEClient.py +12 -6
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/BTClient.py +0 -9
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/__init__.py +1 -1
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/cli.py +12 -19
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/command.py +0 -37
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/explore.py +178 -40
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/packet.py +42 -37
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/parser.py +49 -37
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/serial_client.py +2 -3
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/stream_processor.py +7 -17
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/tools.py +29 -241
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/PKG-INFO +3 -3
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/SOURCES.txt +1 -0
- explorepy-4.3.1/tests/integration_test_ble.py +115 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/.cookiecutterrc +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/.coveragerc +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/.editorconfig +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/AUTHORS.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/CONTRIBUTING.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/LICENSE +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/MANIFEST.in +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/authors.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/changelog.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/contributing.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/explore_legacy_devices.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/index.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/installation.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/logo.jpg +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/readme.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/reference/explorepy.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/reference/index.rst +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/requirements.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/docs/spelling_wordlist.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/setup.cfg +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/_exceptions.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/bt_mock_client.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/bt_mock_server.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/debug.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/filters.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/log_config.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy/settings_manager.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/dependency_links.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/entry_points.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/requires.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/src/explorepy.egg-info/top_level.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/README.md +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/__init__.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/conftest.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/calibration_info +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/calibration_info_usbc +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/cmd_rcv +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/cmd_stat +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/device_info +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/device_info_ble +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/device_info_v2 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/device_info_v2_2 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/disconnect +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg16_ble +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg32 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg94 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg98 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg98_ble +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg98_usbc +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/eeg98_usbc_2 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/env +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/orn +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/orn_2 +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/orn_matrix.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/push_marker +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/in/trigger_in +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/axis_and_angle.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/calibration_info_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/calibration_info_usbc_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/cmd_rcv_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/cmd_stat_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/device_info_ble_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/device_info_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/device_info_v2_2_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/device_info_v2_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/disconnect_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg16_ble_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg32_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg94_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg98_ble_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg98_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg98_out_fake.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg98_usbc_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/eeg98_usbc_out_2.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/env_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/orn_2_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/orn_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/push_marker_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/res/out/trigger_in_out.txt +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tests/test_packet.py +0 -0
- {explorepy-4.2.0 → explorepy-4.3.1}/tox.ini +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
4.3.1 (17.9.2025)
|
|
6
|
+
------------------
|
|
7
|
+
* Bugfix for 32 channel impedance recording
|
|
8
|
+
* Bugfix for BLE connection drop
|
|
9
|
+
* Update binary converter
|
|
10
|
+
|
|
11
|
+
4.3.0 (10.9.2025)
|
|
12
|
+
------------------
|
|
13
|
+
* Live impedance acquisition and recording
|
|
14
|
+
* Improve parser efficiency
|
|
15
|
+
* Bugfix for Linux release
|
|
16
|
+
* Refactor codebase
|
|
17
|
+
|
|
5
18
|
4.2.0 (15.7.2025)
|
|
6
19
|
------------------
|
|
7
20
|
* Add ExplorePy and Arduino interfacing example
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: explorepy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Author-email: MentaLab Hub <support@mentab.org>
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/Mentalab-hub/explorepy
|
|
@@ -62,9 +62,9 @@ Dynamic: license-file
|
|
|
62
62
|
:target: https://pypi.org/project/explorepy
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.
|
|
65
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.3.1.svg
|
|
66
66
|
:alt: Commits since latest release
|
|
67
|
-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.
|
|
67
|
+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.3.1...master
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
.. |wheel| image:: https://img.shields.io/pypi/wheel/explorepy.svg
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
:target: https://pypi.org/project/explorepy
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.
|
|
20
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.3.1.svg
|
|
21
21
|
:alt: Commits since latest release
|
|
22
|
-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.
|
|
22
|
+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.3.1...master
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
.. |wheel| image:: https://img.shields.io/pypi/wheel/explorepy.svg
|
|
@@ -55,6 +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 real-time monitoring (CSV only)
|
|
58
59
|
-h, --help Show this message and exit.
|
|
59
60
|
|
|
60
61
|
|
|
@@ -221,10 +222,13 @@ Recording
|
|
|
221
222
|
You can record data in realtime to EDF (BDF+) or CSV files using:
|
|
222
223
|
::
|
|
223
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.
|
|
224
226
|
|
|
225
|
-
|
|
227
|
+
Enable imp_mode argument to record impedance data as well:
|
|
226
228
|
::
|
|
227
|
-
explore.record_data(file_name='test',
|
|
229
|
+
explore.record_data(file_name='test', duration=120, file_type='csv', imp_mode=True)
|
|
230
|
+
|
|
231
|
+
.. note:: Impedance recording is only supported for CSV files!
|
|
228
232
|
|
|
229
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.
|
|
230
234
|
|
|
@@ -376,34 +380,100 @@ Impedance data acquisition in real-time
|
|
|
376
380
|
"""""""""""""""""""""""""""""""""""""""
|
|
377
381
|
::
|
|
378
382
|
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
import argparse
|
|
384
|
+
import csv
|
|
381
385
|
import time
|
|
386
|
+
|
|
387
|
+
import numpy as np
|
|
388
|
+
import pandas as pd
|
|
389
|
+
import matplotlib.pyplot as plt
|
|
390
|
+
from scipy import signal
|
|
391
|
+
|
|
382
392
|
import explorepy
|
|
383
393
|
from explorepy.stream_processor import TOPICS
|
|
384
394
|
|
|
385
395
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
# ----------------------------- Argument Parsing ----------------------------- #
|
|
397
|
+
parser = argparse.ArgumentParser(description="Acquire and filter impedance-mode ExG data from an Explore device.")
|
|
398
|
+
parser.add_argument("--device-name", required=True, help="Name of the Explore device (e.g., Explore_AAXX)")
|
|
399
|
+
args = parser.parse_args()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ----------------------------- Configuration ----------------------------- #
|
|
403
|
+
FS = 250 # Sampling rate in Hz
|
|
404
|
+
CHANNEL_LABELS = [f"ch{i}" for i in range(1, 33)]
|
|
405
|
+
OUTPUT_FILENAME = "exg_data_imp_mode.csv"
|
|
406
|
+
RECORD_SECONDS = 40
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ----------------------------- CSV Setup ----------------------------- #
|
|
410
|
+
csv_file = open(OUTPUT_FILENAME, 'w', newline='\n')
|
|
411
|
+
csv_writer = csv.writer(csv_file, delimiter=",")
|
|
412
|
+
csv_writer.writerow(['Timestamp'] + CHANNEL_LABELS[:8]) # Log only first 8 channels
|
|
413
|
+
|
|
390
414
|
|
|
415
|
+
# ----------------------------- Packet Handlers ----------------------------- #
|
|
416
|
+
def handle_exg_packet(packet):
|
|
417
|
+
"""Callback to handle incoming ExG data packets."""
|
|
418
|
+
timestamps, signals = packet.get_data(FS)
|
|
419
|
+
data = np.concatenate((np.array(timestamps)[:, np.newaxis].T, np.array(signals)), axis=0)
|
|
420
|
+
np.savetxt(csv_file, np.round(data.T, 4), fmt='%4f', delimiter=',')
|
|
391
421
|
|
|
392
|
-
exp_device = explorepy.Explore()
|
|
393
422
|
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
def handle_impedance_packet(packet):
|
|
424
|
+
"""Callback to handle incoming impedance packets."""
|
|
425
|
+
impedance_values = packet.get_impedances()
|
|
426
|
+
print("Impedance:", impedance_values)
|
|
396
427
|
|
|
397
|
-
exp_device.stream_processor.subscribe(callback=handle_imp, topic=TOPICS.imp)
|
|
398
428
|
|
|
399
|
-
#
|
|
400
|
-
|
|
429
|
+
# ----------------------------- Device Initialization ----------------------------- #
|
|
430
|
+
device = explorepy.Explore()
|
|
431
|
+
device.connect(args.device_name)
|
|
432
|
+
device.stream_processor.subscribe(callback=handle_impedance_packet, topic=TOPICS.imp)
|
|
433
|
+
device.stream_processor.subscribe(callback=handle_exg_packet, topic=TOPICS.raw_ExG)
|
|
434
|
+
device.stream_processor.imp_initialize(notch_freq=50)
|
|
401
435
|
|
|
402
|
-
|
|
403
|
-
|
|
436
|
+
|
|
437
|
+
# ----------------------------- Data Acquisition Loop ----------------------------- #
|
|
438
|
+
for _ in range(RECORD_SECONDS):
|
|
404
439
|
time.sleep(1)
|
|
405
|
-
count += 1
|
|
406
440
|
|
|
407
|
-
|
|
408
|
-
|
|
441
|
+
|
|
442
|
+
# ----------------------------- Cleanup ----------------------------- #
|
|
443
|
+
device.stream_processor.disable_imp()
|
|
444
|
+
device.stream_processor.unsubscribe(callback=handle_impedance_packet, topic=TOPICS.imp)
|
|
445
|
+
device.stream_processor.unsubscribe(callback=handle_exg_packet, topic=TOPICS.raw_ExG)
|
|
446
|
+
csv_file.close()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ----------------------------- Signal Processing ----------------------------- #
|
|
450
|
+
def apply_bandpass_filter(signal_data, low_freq, high_freq, fs, order=3):
|
|
451
|
+
"""Apply a Butterworth bandpass filter to the signal."""
|
|
452
|
+
b, a = signal.butter(order, [low_freq / fs, high_freq / fs], btype='bandpass')
|
|
453
|
+
return signal.filtfilt(b, a, signal_data)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def apply_notch_filter(signal_data, fs, freq=50, quality_factor=30.0):
|
|
457
|
+
"""Apply a notch filter at the specified frequency."""
|
|
458
|
+
b, a = signal.iirnotch(freq, quality_factor, fs)
|
|
459
|
+
return signal.filtfilt(b, a, signal_data)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ----------------------------- Load and Filter Data ----------------------------- #
|
|
463
|
+
df = pd.read_csv(OUTPUT_FILENAME, delimiter=',', dtype=np.float64)
|
|
464
|
+
raw_ch1 = df['ch1']
|
|
465
|
+
filtered = apply_notch_filter(raw_ch1, FS, freq=62.5)
|
|
466
|
+
filtered = apply_notch_filter(filtered, FS, freq=50)
|
|
467
|
+
filtered = apply_bandpass_filter(filtered, low_freq=0.5, high_freq=30, fs=FS)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ----------------------------- Plot Filtered Signal ----------------------------- #
|
|
471
|
+
plt.figure(figsize=(10, 4))
|
|
472
|
+
plt.plot(df['Timestamp'], filtered, label='Filtered ch1')
|
|
473
|
+
plt.xlabel("Time (s)")
|
|
474
|
+
plt.ylabel("Amplitude (µV)")
|
|
475
|
+
plt.title("Filtered EEG Signal - Channel 1")
|
|
476
|
+
plt.grid(True)
|
|
477
|
+
plt.tight_layout()
|
|
478
|
+
plt.show()
|
|
409
479
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import atexit
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
6
7
|
from queue import (
|
|
@@ -189,16 +190,20 @@ class BLEClient(BTClient):
|
|
|
189
190
|
def disconnect(self):
|
|
190
191
|
"""Disconnect from the device"""
|
|
191
192
|
self.is_connected = False
|
|
192
|
-
|
|
193
|
+
if os.name != 'nt':
|
|
194
|
+
if self.client and self.client.is_connected:
|
|
195
|
+
try:
|
|
196
|
+
asyncio.run(self.client.stop_notify(self.eeg_tx_char_uuid))
|
|
197
|
+
except RuntimeError:
|
|
198
|
+
# nothing to do here, this works even though there is an exception
|
|
199
|
+
pass
|
|
200
|
+
if self.notify_task:
|
|
201
|
+
self.notify_task.cancel()
|
|
193
202
|
self.read_event.set()
|
|
194
203
|
time.sleep(1)
|
|
195
204
|
self.stop_read_loop()
|
|
196
205
|
self.ble_device = None
|
|
197
206
|
self.buffer = Queue()
|
|
198
|
-
logger.info('ExplorePy disconnecting from device')
|
|
199
|
-
|
|
200
|
-
def _find_mac_address(self):
|
|
201
|
-
raise NotImplementedError
|
|
202
207
|
|
|
203
208
|
def read(self, n_bytes):
|
|
204
209
|
"""Read n_bytes from the socket
|
|
@@ -221,7 +226,8 @@ class BLEClient(BTClient):
|
|
|
221
226
|
raise ConnectionAbortedError('Error reading data from BLE stream, too many bytes requested')
|
|
222
227
|
return ret
|
|
223
228
|
except Empty:
|
|
224
|
-
|
|
229
|
+
if self.is_connected:
|
|
230
|
+
raise ConnectionAbortedError
|
|
225
231
|
except Exception as error:
|
|
226
232
|
logger.error('Unknown error reading data from BLE stream, error is {}'.format(error))
|
|
227
233
|
raise ConnectionAbortedError(str(error))
|
|
@@ -12,7 +12,6 @@ class BTClient(abc.ABC):
|
|
|
12
12
|
self.mac_address = mac_address
|
|
13
13
|
self.device_name = device_name
|
|
14
14
|
self.bt_serial_port_manager = None
|
|
15
|
-
self.device_manager = None
|
|
16
15
|
|
|
17
16
|
@abc.abstractmethod
|
|
18
17
|
def connect(self):
|
|
@@ -34,10 +33,6 @@ class BTClient(abc.ABC):
|
|
|
34
33
|
def disconnect(self):
|
|
35
34
|
"""Disconnect from the device"""
|
|
36
35
|
|
|
37
|
-
@abc.abstractmethod
|
|
38
|
-
def _find_mac_address(self):
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
36
|
@abc.abstractmethod
|
|
42
37
|
def read(self, n_bytes):
|
|
43
38
|
"""Read n_bytes from the socket
|
|
@@ -56,7 +51,3 @@ class BTClient(abc.ABC):
|
|
|
56
51
|
Args:
|
|
57
52
|
data (bytearray): Data to be sent
|
|
58
53
|
"""
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def _check_mac_address(device_name, mac_address):
|
|
62
|
-
return (device_name[-4:-2] == mac_address[-5:-3]) and (device_name[-2:] == mac_address[-2:])
|
|
@@ -12,8 +12,6 @@ import explorepy
|
|
|
12
12
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
default_bt_backend = explorepy.get_bt_interface()
|
|
16
|
-
|
|
17
15
|
|
|
18
16
|
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
|
|
19
17
|
@click.option("--version", "-V", help="Print explorepy version", is_flag=True)
|
|
@@ -77,13 +75,22 @@ def acquire(name, address, duration):
|
|
|
77
75
|
@click.option("-d", "--duration", type=int, help="Recording duration in seconds", metavar="<integer>")
|
|
78
76
|
@click.option("--edf", 'file_type', flag_value='edf', help="Write in EDF file")
|
|
79
77
|
@click.option("--csv", 'file_type', flag_value='csv', help="Write in csv file (default type)", default=True)
|
|
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=None)
|
|
80
80
|
@verify_inputs
|
|
81
|
-
def record_data(address, name, filename, overwrite, duration, file_type):
|
|
81
|
+
def record_data(address, name, filename, overwrite, duration, file_type, imp_mode, notch_freq):
|
|
82
82
|
"""Record data from Explore to a file"""
|
|
83
83
|
explore = explorepy.explore.Explore()
|
|
84
84
|
explore.connect(mac_address=address, device_name=name)
|
|
85
|
-
explore.record_data(
|
|
86
|
-
|
|
85
|
+
explore.record_data(
|
|
86
|
+
file_name=filename,
|
|
87
|
+
file_type=file_type,
|
|
88
|
+
do_overwrite=overwrite,
|
|
89
|
+
duration=duration,
|
|
90
|
+
imp_mode=imp_mode,
|
|
91
|
+
notch_freq=notch_freq,
|
|
92
|
+
block=True
|
|
93
|
+
)
|
|
87
94
|
|
|
88
95
|
|
|
89
96
|
@cli.command()
|
|
@@ -153,17 +160,3 @@ def soft_reset(address, name):
|
|
|
153
160
|
explore = explorepy.explore.Explore()
|
|
154
161
|
explore.connect(mac_address=address, device_name=name)
|
|
155
162
|
explore.reset_soft()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@cli.command()
|
|
159
|
-
@click.option("--address", "-a", type=str, help="Explore device's MAC address")
|
|
160
|
-
@click.option("--name", "-n", type=str, help="Name of the device")
|
|
161
|
-
@click.option("-m", "--channel-mask", type=str, required=True,
|
|
162
|
-
help="Channel mask, it should be a binary string containing 1 and 0, "
|
|
163
|
-
"representing the mask (LSB is channel 1).")
|
|
164
|
-
@verify_inputs
|
|
165
|
-
def set_channels(address, name, channel_mask):
|
|
166
|
-
"""Mask the channels of the Explore device"""
|
|
167
|
-
explore = explorepy.explore.Explore()
|
|
168
|
-
explore.connect(mac_address=address, device_name=name)
|
|
169
|
-
explore.set_channels(channel_mask)
|
|
@@ -23,23 +23,12 @@ class OpcodeID(Enum):
|
|
|
23
23
|
CMD_SPS_SET = b'\xA1'
|
|
24
24
|
CMD_CH_SET = b'\xA2'
|
|
25
25
|
CMD_MEM_FORMAT = b'\xA3'
|
|
26
|
-
CMD_REC_TIME_SET = b'\xB1'
|
|
27
|
-
CMD_MODULE_DISABLE = b'\xA4'
|
|
28
|
-
CMD_MODULE_ENABLE = b'\xA5'
|
|
29
26
|
CMD_ZM_DISABLE = b'\xA6'
|
|
30
27
|
CMD_ZM_ENABLE = b'\xA7'
|
|
31
28
|
CMD_SOFT_RESET = b'\xA8'
|
|
32
29
|
CMD_TEST_SIG = b'\xAA'
|
|
33
30
|
|
|
34
31
|
|
|
35
|
-
class Result(Enum):
|
|
36
|
-
"""Results of the command execution"""
|
|
37
|
-
API_CMD_SUCCESSFUL = b'\x01'
|
|
38
|
-
API_CMD_ILLEGAL = b'\x02'
|
|
39
|
-
API_CMD_FAILED = b'\x00'
|
|
40
|
-
API_CMD_NA = b'\xFF'
|
|
41
|
-
|
|
42
|
-
|
|
43
32
|
class DeviceConfiguration:
|
|
44
33
|
"""Device Configuration Class"""
|
|
45
34
|
|
|
@@ -52,10 +41,6 @@ class DeviceConfiguration:
|
|
|
52
41
|
self._last_ack_message = None
|
|
53
42
|
self._last_status_message = None
|
|
54
43
|
|
|
55
|
-
def get_device_info(self):
|
|
56
|
-
"""Get device information including adc mask, sampling rate and firmware version."""
|
|
57
|
-
raise NotImplementedError
|
|
58
|
-
|
|
59
44
|
def change_setting(self, command):
|
|
60
45
|
"""Change the settings of the device based on the input command
|
|
61
46
|
|
|
@@ -147,8 +132,6 @@ class Command:
|
|
|
147
132
|
self.opcode = None
|
|
148
133
|
self.param = None
|
|
149
134
|
self.fletcher = b'\xaf\xbe\xad\xde'
|
|
150
|
-
|
|
151
|
-
self.delivery_state = None
|
|
152
135
|
self.result = None
|
|
153
136
|
|
|
154
137
|
def translate(self):
|
|
@@ -266,26 +249,6 @@ class ZMeasurementEnable(Command2B):
|
|
|
266
249
|
return "Impedance measurement enable command"
|
|
267
250
|
|
|
268
251
|
|
|
269
|
-
class SetCh(Command2B):
|
|
270
|
-
"""Change channel mask command"""
|
|
271
|
-
|
|
272
|
-
def __init__(self, ch_mask):
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
Args:
|
|
276
|
-
ch_mask (int): ExG channel mask. It should be integers between 1 and 255.
|
|
277
|
-
"""
|
|
278
|
-
super().__init__()
|
|
279
|
-
self.opcode = OpcodeID.CMD_CH_SET
|
|
280
|
-
if 1 <= ch_mask <= 255:
|
|
281
|
-
self.param = bytes([ch_mask])
|
|
282
|
-
else:
|
|
283
|
-
raise ValueError("Invalid input")
|
|
284
|
-
|
|
285
|
-
def __str__(self):
|
|
286
|
-
return "Channel set command"
|
|
287
|
-
|
|
288
|
-
|
|
289
252
|
class SoftReset(Command2B):
|
|
290
253
|
"""Reset the setting of the device."""
|
|
291
254
|
|