Skip to content

Commit f0138fa

Browse files
authored
Merge pull request #419 from Mentalab-hub/develop
Version 4.4.0
2 parents 25bf50c + ef38b93 commit f0138fa

12 files changed

Lines changed: 200 additions & 126 deletions

File tree

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.3.1.svg
20+
.. |commits-since| image:: https://img.shields.io/github/commits-since/Mentalab-hub/explorepy/v4.4.0.svg
2121
:alt: Commits since latest release
22-
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.3.1...master
22+
:target: https://github.com/Mentalab-hub/explorepy/compare/v4.4.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.3.1'
31+
version = release = '4.4.0'
3232
pygments_style = 'trac'
3333
templates_path = ['.']
3434
extlinks = {

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.3.1
3+
version=4.4.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.3.1
29+
explorepy==4.4.0
3030
fonttools==4.42.1
3131
idna==3.4
3232
importlib-resources==6.0.1

pyproject.toml

Lines changed: 1 addition & 1 deletion
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.3.1"
7+
version = "4.4.0"
88
license = { text = "MIT" }
99
readme = { file = "README.rst", content-type = "text/markdown" }
1010
authors = [

src/explorepy/BLEClient.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from explorepy._exceptions import (
1919
BleDisconnectionError,
20+
BleDisconnectionFailedError,
2021
DeviceNotFoundError,
2122
UnexpectedConnectionError
2223
)
@@ -200,7 +201,22 @@ def disconnect(self):
200201
if self.notify_task:
201202
self.notify_task.cancel()
202203
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+
204220
self.stop_read_loop()
205221
self.ble_device = None
206222
self.buffer = Queue()

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.3.1'
23+
__version__ = '4.4.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
@@ -55,6 +55,13 @@ class BleDisconnectionError(Exception):
5555
pass
5656

5757

58+
class BleDisconnectionFailedError(Exception):
59+
"""
60+
Exception for client fails to achieve disconnected state
61+
"""
62+
pass
63+
64+
5865
class ExplorePyDeprecationError(Exception):
5966
def __init__(self, message="Explorepy support for legacy devices is deprecated.\n"
6067
"Please install explorepy 3.2.1 from Github or use the following command from Anaconda "

src/explorepy/packet.py

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import numba as nb
99
import numpy as np
1010

11-
import explorepy.tools
1211
from explorepy._exceptions import FletcherError
1312

1413

@@ -44,6 +43,7 @@ class PACKET_ID(IntEnum):
4443
PUSHMARKER = 194
4544
CALIBINFO = 195
4645
CALIBINFO_USBC = 197
46+
CALIBINFO_PRO = 196
4747
TRIGGER_OUT = 177 # Trigger-out of Explore device
4848
TRIGGER_IN = 178 # Trigger-in to Explore device
4949
VERSION_INFO = 199
@@ -226,15 +226,19 @@ def int32_to_status(data):
226226
}
227227
return status
228228

229-
def calculate_impedance(self, imp_calib_info):
229+
def calculate_impedance(self, imp_calib_info, index=None):
230230
"""calculate impedance with the help of impedance calibration info
231231
232232
Args:
233233
imp_calib_info (dict): dictionary of impedance calibration info including slope, offset and noise level
234234
235235
"""
236-
scale = imp_calib_info["slope"]
237-
offset = imp_calib_info["offset"]
236+
if index is None:
237+
scale = imp_calib_info["slope"]
238+
offset = imp_calib_info["offset"]
239+
else:
240+
scale = imp_calib_info["slope"][index]
241+
offset = imp_calib_info["offset"][index]
238242
self.imp_data = np.round(
239243
(self.get_ptp()
240244
- imp_calib_info["noise_level"]) * scale / 1.0e6 - offset,
@@ -576,13 +580,6 @@ def __init__(self, timestamp, payload, time_offset=0):
576580
super().__init__(timestamp, payload, time_offset)
577581

578582
def _convert(self, bin_data):
579-
precise_ts = np.ndarray.item(
580-
np.frombuffer(bin_data,
581-
dtype=np.dtype(np.uint32).newbyteorder("<"),
582-
count=1,
583-
offset=0))
584-
scale = 100000 if explorepy.tools.is_explore_pro_device() else 10000
585-
self.timestamp = precise_ts / scale + self._time_offset
586583
code = np.ndarray.item(
587584
np.frombuffer(bin_data,
588585
dtype=np.dtype(np.uint16).newbyteorder("<"),
@@ -710,35 +707,61 @@ def __str__(self):
710707

711708

712709
class CalibrationInfoBase(Packet):
713-
@abc.abstractmethod
714-
def _convert(self, bin_data, offset_multiplier=0.001):
715-
slope = np.frombuffer(bin_data,
716-
dtype=np.dtype(np.uint16).newbyteorder("<"),
717-
count=1,
718-
offset=0).item()
719-
self.slope = slope * 10.0
720-
offset = np.frombuffer(bin_data,
721-
dtype=np.dtype(np.uint16).newbyteorder("<"),
722-
count=1,
723-
offset=2).item()
724-
self.offset = offset * offset_multiplier
710+
"""Base class for calibration packets"""
711+
712+
channels = 4
713+
offset_multiplier = 0.001
714+
715+
def __init__(self, timestamp, payload, time_offset=0):
716+
# Must exist before Packet.__init__ calls _convert()
717+
self.slope = []
718+
self.offset = []
719+
super().__init__(timestamp, payload, time_offset)
720+
721+
def _convert(self, bin_data):
722+
dtype_u16 = np.dtype("<u2")
723+
calib_pair_count = len(bin_data) // 4
724+
for i in range(self.channels):
725+
if calib_pair_count <= i:
726+
# Copy first value
727+
self.slope.append(self.slope[0])
728+
self.offset.append(self.offset[0])
729+
continue
730+
base = i * 4
731+
732+
slope = np.frombuffer(
733+
bin_data,
734+
dtype=dtype_u16,
735+
count=1,
736+
offset=base
737+
).item()
738+
self.slope.append(slope * 10.0)
739+
740+
offset = np.frombuffer(
741+
bin_data,
742+
dtype=dtype_u16,
743+
count=1,
744+
offset=base + 2
745+
).item()
746+
self.offset.append(offset * self.offset_multiplier)
725747

726748
def get_info(self):
727-
"""Get calibration info"""
728749
return {"slope": self.slope, "offset": self.offset}
729750

730751
def __str__(self):
731-
return "calibration info: slope = " + str(self.slope) + "\toffset = " + str(self.offset)
752+
return f"calibration info: slope = {self.slope}\toffset = {self.offset}"
732753

733754

734755
class CalibrationInfo(CalibrationInfoBase):
735-
def _convert(self, bin_data):
736-
super()._convert(bin_data, offset_multiplier=0.001)
756+
offset_multiplier = 0.001
737757

738758

739759
class CalibrationInfo_USBC(CalibrationInfoBase):
740-
def _convert(self, bin_data):
741-
super()._convert(bin_data, offset_multiplier=0.01)
760+
offset_multiplier = 0.01
761+
762+
763+
class CalibrationInfoPro(CalibrationInfoBase):
764+
offset_multiplier = 0.01
742765

743766

744767
class BleImpedancePacket(EEG98_USBC):
@@ -759,6 +782,19 @@ def populate_packet_with_data(self, ble_packet_list):
759782
data_array = np.concatenate((data_array, data), axis=1)
760783
self.data = data_array
761784

785+
def resize_packet(self, full_data, index):
786+
self.data = full_data[index * 8: index * 8 + 8, :]
787+
788+
def populate_data_1d(self, ble_packet_list):
789+
data_array = None
790+
for i in range(len(ble_packet_list)):
791+
_, data = ble_packet_list[i].get_data()
792+
if data_array is None:
793+
data_array = data
794+
else:
795+
data_array = np.concatenate((data_array, data), axis=0)
796+
self.data = data_array
797+
762798

763799
class VersionInfoPacket(Packet):
764800
def __init__(self, timestamp, payload, time_offset=0):
@@ -800,6 +836,7 @@ def __str__(self):
800836
PACKET_ID.CMDSTAT: CommandStatus,
801837
PACKET_ID.CALIBINFO: CalibrationInfo,
802838
PACKET_ID.CALIBINFO_USBC: CalibrationInfo_USBC,
839+
PACKET_ID.CALIBINFO_PRO: CalibrationInfoPro,
803840
PACKET_ID.PUSHMARKER: PushButtonMarker,
804841
PACKET_ID.TRIGGER_IN: TriggerIn,
805842
PACKET_ID.TRIGGER_OUT: TriggerOut,

src/explorepy/parser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ def _stream_loop(self):
204204
except EOFError:
205205
logger.info('End of file')
206206
self.stop_streaming()
207+
except AttributeError:
208+
if self.stream_interface is None:
209+
# device already disconnected
210+
pass
207211
except Exception as error:
208212
logger.critical('Unexpected error: ', error)
209213
self.stop_streaming()

src/explorepy/settings_manager.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717

1818
class SettingsManager:
19+
_fw_ch_count = {"7": 8, "8": 16, "9": 32}
20+
_board_ch_count = {"PCB_304_801_XXX": 32, "PCB_305_801_XXX": 16, "PCB_303_801E_XX": 8,
21+
"PCB_304_801p2_X": 32, "PCB_304_891p2_X": 16}
22+
1923
def __init__(self, name):
2024
self.settings_dict = None
2125

@@ -28,6 +32,7 @@ def __init__(self, name):
2832
pass
2933
self.hardware_channel_mask_key = "hardware_mask"
3034
self.software_channel_mask_key = "software_mask"
35+
self.firmware_version_key = "firmware_version"
3136
self.adc_mask_key = "adc_mask"
3237
self.channel_name_key = "channel_name"
3338
self.channel_count_key = "channel_count"
@@ -105,52 +110,30 @@ def update_device_settings(self, device_info_dict_update):
105110
self.load_current_settings()
106111
for key, value in device_info_dict_update.items():
107112
self.settings_dict[key] = value
108-
if "board_id" in device_info_dict_update:
109-
if self.settings_dict["board_id"] == "PCB_304_801_XXX":
110-
self.settings_dict[self.channel_count_key] = 32
111-
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(32)]
112-
if self.software_channel_mask_key not in self.settings_dict:
113-
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
114-
self.settings_dict[self.software_channel_mask_key] = hardware_adc
115-
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
116-
if "board_id" in device_info_dict_update:
117-
if self.settings_dict["board_id"] == "PCB_305_801_XXX":
118-
self.settings_dict[self.channel_count_key] = 16
119-
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(16)]
120-
if self.software_channel_mask_key not in self.settings_dict:
121-
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
122-
self.settings_dict[self.software_channel_mask_key] = hardware_adc
123-
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
124-
if "board_id" in device_info_dict_update:
125-
# 8 channel BLE board
126-
if self.settings_dict["board_id"] == "PCB_303_801E_XXX":
127-
self.settings_dict[self.channel_count_key] = 8
128-
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(8)]
129-
if self.software_channel_mask_key not in self.settings_dict:
130-
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
131-
self.settings_dict[self.software_channel_mask_key] = hardware_adc
132-
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
133-
if "board_id" in device_info_dict_update:
134-
# 32 channel BLE board
135-
if self.settings_dict["board_id"] == "PCB_304_801p2_X":
136-
self.settings_dict[self.channel_count_key] = 32
137-
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(32)]
138-
if self.software_channel_mask_key not in self.settings_dict:
139-
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
140-
self.settings_dict[self.software_channel_mask_key] = hardware_adc
141-
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
142-
if "board_id" in device_info_dict_update:
143-
# 32 channel BLE board
144-
if self.settings_dict["board_id"] == "PCB_304_891p2_X":
145-
self.settings_dict[self.channel_count_key] = 16
146-
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(16)]
147-
if self.software_channel_mask_key not in self.settings_dict:
148-
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
149-
self.settings_dict[self.software_channel_mask_key] = hardware_adc
150-
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
113+
ch_count = -1
114+
if self.firmware_version_key in device_info_dict_update:
115+
fw = device_info_dict_update[self.firmware_version_key]
116+
major = fw.split(".")[0]
117+
if major in self._fw_ch_count:
118+
ch_count = self._fw_ch_count[major]
119+
if ch_count == -1:
120+
logger.warn("Could not retrieve channel count from firmware version, attempting to get channel count from "
121+
"board ID...")
122+
# fallback to PCB ID
123+
if self.board_id_key in device_info_dict_update:
124+
for key in self._board_ch_count:
125+
if self.settings_dict["board_id"] == key:
126+
ch_count = self._board_ch_count[key]
127+
if ch_count != -1:
128+
self.settings_dict[self.channel_count_key] = ch_count
129+
self.settings_dict[self.hardware_channel_mask_key] = [1 for _ in range(ch_count)]
130+
if self.software_channel_mask_key not in self.settings_dict:
131+
hardware_adc = self.settings_dict.get(self.hardware_channel_mask_key)
132+
self.settings_dict[self.software_channel_mask_key] = hardware_adc
133+
self.settings_dict[self.adc_mask_key] = self.settings_dict.get(self.software_channel_mask_key)
151134

152135
if self.channel_count_key not in self.settings_dict:
153-
self.settings_dict[self.channel_count_key] = 8 if sum(self.settings_dict["adc_mask"]) > 4 else 4
136+
raise KeyError("Channel count could not be set from firmware or hardware version!")
154137
if self.channel_name_key not in self.settings_dict:
155138
self.settings_dict[self.channel_name_key] = [f'ch{i + 1}' for i in
156139
range(self.settings_dict[self.channel_count_key])]

0 commit comments

Comments
 (0)