Skip to content

Commit 1b158d2

Browse files
authored
Merge pull request #374 from Mentalab-hub/develop
Release 4.1.0
2 parents 4ab0aa3 + a3215b4 commit 1b158d2

15 files changed

+151
-74
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 4.0.0
2+
current_version = 4.1.0
33
commit = False
44
tag = False
55

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
Changelog
33
=========
44

5+
4.1.0 (31.3.2025)
6+
------------------
7+
* Improve bluetooth connection UX
8+
* Add more data in IMU packet
9+
* Improve USB streaming
10+
11+
512
4.0.0 (14.3.2025)
613
------------------
714
* Deprecate support for Explore legacy devices

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
:target: https://pypi.org/project/explorepy
1818

1919

20-
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.0.0.svg
20+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.1.0.svg
2121
:alt: Commits since latest release
22-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.0.0...master
22+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.1.0...master
2323

2424

2525
.. |wheel| image:: https://img.shields.io/pypi/wheel/explorepy.svg

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
year = '2018-2025'
2929
author = 'Mentalab GmbH.'
3030
copyright = '{0}, {1}'.format(year, author)
31-
version = release = '4.0.0'
31+
version = release = '4.1.0'
3232
pygments_style = 'trac'
3333
templates_path = ['.']
3434
extlinks = {

examples/markers_examples/send_8_bit_marker_during_connection_example.py

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import explorepy
22
import time
33

4-
# for independent markers to be stored only in binary file, we only need to instantiate Explore class!
4+
# for independent markers, we only need to instantiate Explore class!
55
exp_device = explorepy.Explore()
66

77
n_markers = 20
88
interval = 2
99
for i in range(n_markers):
1010
exp_device.send_8_bit_trigger(i)
11-
time.sleep(2)
11+
time.sleep(interval)
1212

installer/windows/installer.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[Application]
22
name=MentaLab ExplorePy
3-
version=4.0.0
3+
version=4.1.0
44
entry_point=explorepy.cli:cli
55
console=true
66
icon=mentalab.ico
@@ -26,7 +26,7 @@ pypi_wheels =
2626
decorator==5.1.1
2727
distlib==0.3.7
2828
eeglabio==0.0.2.post4
29-
explorepy==4.0.0
29+
explorepy==4.1.0
3030
fonttools==4.42.1
3131
idna==3.4
3232
importlib-resources==6.0.1

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
44

55
[project]
66
name = 'explorepy'
7-
version = "4.0.0"
7+
version = "4.1.0"
88
license = { text = "MIT" }
99
readme = { file = "README.rst", content-type = "text/markdown" }
1010
authors = [
@@ -25,7 +25,6 @@ classifiers = [
2525
"Operating System :: Microsoft :: Windows",
2626
"Operating System :: POSIX :: Linux",
2727
"Topic :: Scientific/Engineering :: Information Analysis",
28-
"Topic :: Scientific/Engineering :: Neuroscience App",
2928
"Topic :: Software Development :: Libraries :: Python Modules"
3029
]
3130

src/explorepy/BLEClient.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
from explorepy._exceptions import (
1818
BleDisconnectionError,
19-
DeviceNotFoundError
19+
DeviceNotFoundError,
20+
UnexpectedConnectionError
2021
)
2122
from explorepy.BTClient import BTClient
2223

@@ -108,7 +109,10 @@ def connect(self):
108109
self.notification_thread = threading.Thread(target=self.start_read_loop, daemon=True)
109110
self.notification_thread.start()
110111
print('waiting for BLE device to show up..')
111-
self.result_queue.get()
112+
ret = self.result_queue.get()
113+
if not ret:
114+
logger.error("Got exception in read loop")
115+
raise UnexpectedConnectionError("Could not connect to the device")
112116

113117
if self.ble_device is None:
114118
logger.info('No device found!!')
@@ -122,13 +126,16 @@ def connect(self):
122126
def start_read_loop(self):
123127
try:
124128
asyncio.new_event_loop().run_until_complete(self.ble_manager())
129+
self.result_queue.put(True)
125130
except RuntimeError as error:
126131
logger.info('Shutting down BLE stream loop with error {}'.format(error))
127132
except asyncio.exceptions.CancelledError as error:
128133
logger.debug('asyncio.exceptions.CancelledError from BLE stream thread {}'.format(error))
129134
except BleDisconnectionError as error:
130135
print('Got error as {}'.format(error))
131136
raise error
137+
finally:
138+
self.result_queue.put(False)
132139

133140
def stop_read_loop(self):
134141
logger.debug('Stopping BLE stream loop')
@@ -152,6 +159,7 @@ async def ble_manager(self):
152159
raise error
153160
except Exception as error:
154161
logger.debug('Got an BLE exception with error {}'.format(error))
162+
raise error
155163

156164
async def _discover_device(self):
157165
if self.mac_address:

src/explorepy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121

2222
__all__ = ["Explore", "command", "tools", "log_config"]
23-
__version__ = '4.0.0'
23+
__version__ = '4.1.0'
2424

2525
this = sys.modules[__name__]
2626
# TODO appropriate library

src/explorepy/_exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
import sys
77

88

9+
class UnexpectedConnectionError(ConnectionError):
10+
"""
11+
Generic exception thrown if an unexpected error occurs during connection
12+
"""
13+
pass
14+
15+
916
class InputError(Exception):
1017
"""
1118
User input exception

src/explorepy/explore.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
create_marker_recorder,
4646
create_meta_recorder,
4747
create_orn_recorder,
48+
get_orn_chan_len,
4849
local_clock,
4950
setup_usb_marker_port
5051
)
@@ -187,7 +188,8 @@ def record_data(
187188
exg_ch=exg_ch_names)
188189
self.recorders['orn'] = create_orn_recorder(filename=orn_out_file,
189190
file_type=file_type,
190-
do_overwrite=do_overwrite)
191+
do_overwrite=do_overwrite,
192+
n_chan=get_orn_chan_len(self.stream_processor.device_info))
191193

192194
# TODO: make sure older timestamp in meta file was not used in any other software!
193195
if file_type == 'csv':
@@ -323,7 +325,8 @@ def convert_bin(self, bin_file, out_dir='', file_type='edf', do_overwrite=False,
323325
do_overwrite=do_overwrite, batch_mode=True)
324326
self.recorders['orn'] = create_orn_recorder(filename=orn_out_file,
325327
file_type=self.recorders['file_type'],
326-
do_overwrite=do_overwrite, batch_mode=True)
328+
do_overwrite=do_overwrite, batch_mode=True,
329+
n_chan=get_orn_chan_len(self.stream_processor.device_info))
327330

328331
if self.recorders['file_type'] == 'csv':
329332
self.recorders['marker'] = create_marker_recorder(
@@ -424,10 +427,10 @@ def stream_progress_handler(progress):
424427
logger.info('Conversion process terminated.')
425428

426429
def push2lsl(self, duration=None, block=False):
427-
r"""Push samples to two lsl streams (ExG and ORN streams)
430+
"""Push samples to three lsl streams (ExG, Marker and ORN streams)
428431
429432
Args:
430-
duration (float): duration of data acquiring (if None it streams for one hour).
433+
duration (float): duration of data acquiring (if None it streams for three hours).
431434
block (bool): blocking mode
432435
"""
433436
self._check_connection()
@@ -484,7 +487,7 @@ def set_marker(self, marker_string, time_lsl=None):
484487
def send_8_bit_trigger(self, eight_bit_value):
485488
eight_bit_value = eight_bit_value % 256
486489
trigger_id = 0xAB
487-
cmd = [trigger_id, eight_bit_value, 1, 2, 3, 4, 5, 6, 7, 8, 0xDE, 0xAD, 0xBE, 0xEF]
490+
cmd = [trigger_id, eight_bit_value, 1, 2, 3, 4, 5, 6, 7, 8, 0xAF, 0xBE, 0xAD, 0xDE]
488491
explore_port = setup_usb_marker_port()
489492
explore_port.write(bytearray(cmd))
490493
explore_port.close()

src/explorepy/packet.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
class PACKET_ID(IntEnum):
2020
"""Packet ID enum"""
2121

22-
ORN = 13
22+
ORN_V1 = 13
23+
ORN_V2 = 14
2324
ENV = 19
2425
TS = 27
2526
DISCONNECT = 111
@@ -332,13 +333,18 @@ def __init__(self, timestamp, payload, time_offset=0):
332333

333334

334335
class Orientation(Packet):
335-
"""Orientation data packet"""
336-
337336
def __init__(self, timestamp, payload, time_offset=0):
338337
super().__init__(timestamp, payload, time_offset)
339338
self.theta = None
340339
self.rot_axis = None
341340

341+
342+
class OrientationV1(Orientation):
343+
"""Orientation data packet"""
344+
345+
def __init__(self, timestamp, payload, time_offset=0):
346+
super().__init__(timestamp, payload, time_offset)
347+
342348
def _convert(self, bin_data):
343349
data = np.copy(
344350
np.frombuffer(bin_data, dtype=np.dtype(
@@ -372,6 +378,51 @@ def compute_angle(self, matrix=None):
372378
return [theta, rot_axis]
373379

374380

381+
class OrientationV2(Orientation):
382+
"""Orientation data packet"""
383+
384+
def __init__(self, timestamp, payload, time_offset=0):
385+
self.quat = None
386+
super().__init__(timestamp, payload, time_offset)
387+
388+
def _convert(self, bin_data):
389+
data = np.copy(
390+
np.frombuffer(bin_data[0:18], dtype=np.dtype(
391+
np.int16).newbyteorder("<"))).astype(float)
392+
self.acc = 0.122 * data[0:3] # Unit [mg/LSB]
393+
self.gyro = 70 * data[3:6] # Unit [mdps/LSB]
394+
self.mag = 1.52 * np.multiply(data[6:9], np.array(
395+
[-1, 1, 1])) # Unit [mgauss/LSB]
396+
data = np.copy(
397+
np.frombuffer(bin_data[18:34], dtype=np.dtype(
398+
np.float32).newbyteorder("<"))).astype(float)
399+
self.quat = data
400+
self.theta = None
401+
self.rot_axis = None
402+
403+
def __str__(self):
404+
return "Acc: " + str(self.acc) + "\tGyro: " + str(self.gyro) + "\tMag: " + str(
405+
self.mag) + "\tQuat: " + str(self.quat)
406+
407+
def get_data(self, srate=None):
408+
"""Get orientation timestamp and data"""
409+
return [self.timestamp
410+
], self.acc.tolist() + self.gyro.tolist() + self.mag.tolist() + self.quat.tolist()
411+
412+
def compute_angle(self, matrix=None):
413+
"""Compute physical angle"""
414+
trace = matrix[0][0] + matrix[1][1] + matrix[2][2]
415+
theta = np.arccos((trace - 1) / 2) * 57.2958
416+
nx = matrix[2][1] - matrix[1][2]
417+
ny = matrix[0][2] - matrix[2][0]
418+
nz = matrix[1][0] - matrix[0][1]
419+
rot_axis = 1 / np.sqrt(
420+
(3 - trace) * (1 + trace)) * np.array([nx, ny, nz])
421+
self.theta = theta
422+
self.rot_axis = rot_axis
423+
return [theta, rot_axis]
424+
425+
375426
class Environment(Packet):
376427
"""Environment data packet"""
377428

@@ -723,7 +774,8 @@ def __str__(self):
723774

724775

725776
PACKET_CLASS_DICT = {
726-
PACKET_ID.ORN: Orientation,
777+
PACKET_ID.ORN_V1: OrientationV1,
778+
PACKET_ID.ORN_V2: OrientationV2,
727779
PACKET_ID.ENV: Environment,
728780
PACKET_ID.TS: TimeStamp,
729781
PACKET_ID.DISCONNECT: Disconnect,

0 commit comments

Comments
 (0)