Skip to content

Commit eb29624

Browse files
authored
Merge pull request #414 from Mentalab-hub/imp-calibration-32ch-pro
Impedance calibration update for pro devices
2 parents 44361ba + 0d6fe32 commit eb29624

3 files changed

Lines changed: 134 additions & 66 deletions

File tree

src/explorepy/packet.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class PACKET_ID(IntEnum):
4343
PUSHMARKER = 194
4444
CALIBINFO = 195
4545
CALIBINFO_USBC = 197
46+
CALIBINFO_PRO = 196
4647
TRIGGER_OUT = 177 # Trigger-out of Explore device
4748
TRIGGER_IN = 178 # Trigger-in to Explore device
4849
VERSION_INFO = 199
@@ -225,15 +226,19 @@ def int32_to_status(data):
225226
}
226227
return status
227228

228-
def calculate_impedance(self, imp_calib_info):
229+
def calculate_impedance(self, imp_calib_info, index=None):
229230
"""calculate impedance with the help of impedance calibration info
230231
231232
Args:
232233
imp_calib_info (dict): dictionary of impedance calibration info including slope, offset and noise level
233234
234235
"""
235-
scale = imp_calib_info["slope"]
236-
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]
237242
self.imp_data = np.round(
238243
(self.get_ptp()
239244
- imp_calib_info["noise_level"]) * scale / 1.0e6 - offset,
@@ -702,35 +707,57 @@ def __str__(self):
702707

703708

704709
class CalibrationInfoBase(Packet):
705-
@abc.abstractmethod
706-
def _convert(self, bin_data, offset_multiplier=0.001):
707-
slope = np.frombuffer(bin_data,
708-
dtype=np.dtype(np.uint16).newbyteorder("<"),
709-
count=1,
710-
offset=0).item()
711-
self.slope = slope * 10.0
712-
offset = np.frombuffer(bin_data,
713-
dtype=np.dtype(np.uint16).newbyteorder("<"),
714-
count=1,
715-
offset=2).item()
716-
self.offset = offset * offset_multiplier
710+
"""Base class for calibration packets"""
711+
712+
channels = 1
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+
724+
for i in range(self.channels):
725+
base = i * 4
726+
727+
slope = np.frombuffer(
728+
bin_data,
729+
dtype=dtype_u16,
730+
count=1,
731+
offset=base
732+
).item()
733+
self.slope.append(slope * 10.0)
734+
735+
offset = np.frombuffer(
736+
bin_data,
737+
dtype=dtype_u16,
738+
count=1,
739+
offset=base + 2
740+
).item()
741+
self.offset.append(offset * self.offset_multiplier)
717742

718743
def get_info(self):
719-
"""Get calibration info"""
720744
return {"slope": self.slope, "offset": self.offset}
721745

722746
def __str__(self):
723-
return "calibration info: slope = " + str(self.slope) + "\toffset = " + str(self.offset)
747+
return f"calibration info: slope = {self.slope}\toffset = {self.offset}"
724748

725749

726750
class CalibrationInfo(CalibrationInfoBase):
727-
def _convert(self, bin_data):
728-
super()._convert(bin_data, offset_multiplier=0.001)
751+
offset_multiplier = 0.001
729752

730753

731754
class CalibrationInfo_USBC(CalibrationInfoBase):
732-
def _convert(self, bin_data):
733-
super()._convert(bin_data, offset_multiplier=0.01)
755+
offset_multiplier = 0.01
756+
757+
758+
class CalibrationInfoPro(CalibrationInfoBase):
759+
channels = 4
760+
offset_multiplier = 0.01
734761

735762

736763
class BleImpedancePacket(EEG98_USBC):
@@ -751,6 +778,19 @@ def populate_packet_with_data(self, ble_packet_list):
751778
data_array = np.concatenate((data_array, data), axis=1)
752779
self.data = data_array
753780

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

755795
class VersionInfoPacket(Packet):
756796
def __init__(self, timestamp, payload, time_offset=0):
@@ -792,6 +832,7 @@ def __str__(self):
792832
PACKET_ID.CMDSTAT: CommandStatus,
793833
PACKET_ID.CALIBINFO: CalibrationInfo,
794834
PACKET_ID.CALIBINFO_USBC: CalibrationInfo_USBC,
835+
PACKET_ID.CALIBINFO_PRO: CalibrationInfoPro,
795836
PACKET_ID.PUSHMARKER: PushButtonMarker,
796837
PACKET_ID.TRIGGER_IN: TriggerIn,
797838
PACKET_ID.TRIGGER_OUT: TriggerOut,

src/explorepy/stream_processor.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
from explorepy.filters import ExGFilter
2424
from explorepy.packet import (
2525
EEG,
26-
CalibrationInfo,
27-
CalibrationInfo_USBC,
26+
CalibrationInfoBase,
2827
CommandRCV,
2928
CommandStatus,
3029
DeviceInfo,
@@ -173,8 +172,8 @@ def group_packets_by_base_type(self, packet_batch: List[Tuple]):
173172
base_class = Environment
174173
elif isinstance(packet, EventMarker):
175174
base_class = EventMarker
176-
elif isinstance(packet, CalibrationInfo):
177-
base_class = CalibrationInfo
175+
elif isinstance(packet, CalibrationInfoBase):
176+
base_class = CalibrationInfoBase
178177
elif isinstance(packet, PacketBIN):
179178
base_class = PacketBIN
180179
else:
@@ -225,8 +224,8 @@ def process_batch(self, packet_batch: List[Tuple]):
225224
self._process_marker_batch(sorted_eeg_packets)
226225

227226
# Process calibration info packets
228-
if CalibrationInfo in grouped_packets:
229-
self._process_calib_info_batch(grouped_packets[CalibrationInfo])
227+
if CalibrationInfoBase in grouped_packets:
228+
self._process_calib_info_batch(grouped_packets[CalibrationInfoBase])
230229

231230
# Process binary packets
232231
if PacketBIN in grouped_packets:
@@ -348,7 +347,7 @@ def process(self, packet):
348347
self.dispatch(topic=TOPICS.env, packet=packet)
349348
elif isinstance(packet, EventMarker):
350349
self.dispatch(topic=TOPICS.marker, packet=packet)
351-
elif isinstance(packet, CalibrationInfo) or isinstance(packet, CalibrationInfo_USBC):
350+
elif isinstance(packet, CalibrationInfoBase):
352351
self.imp_calib_info = packet.get_info()
353352
elif not packet:
354353
self.is_connected = False
@@ -449,11 +448,12 @@ def configure_device(self, cmd):
449448
self.start_cmd_process_thread()
450449
return self._device_configurator.change_setting(cmd)
451450

452-
def imp_initialize(self, notch_freq):
451+
def imp_initialize(self, notch_freq, calibration=False):
453452
"""Activate impedance mode in the device"""
454453
logger.info("Starting impedance measurement mode...")
455454
cmd = ZMeasurementEnable()
456455
if self.configure_device(cmd):
456+
self.imp_calib_info['calibration'] = calibration
457457
self.imp_calculator = ImpedanceMeasurement(device_info=self.device_info,
458458
calib_param=self.imp_calib_info,
459459
notch_freq=notch_freq)

src/explorepy/tools.py

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,7 @@ def __init__(self, device_info, stream_name=None):
438438
self.marker_outlet = None
439439
self.exg_outlet = None
440440
self.orn_outlet = None
441-
self.adc_mask = SettingsManager(
442-
device_info["device_name"]).get_adc_mask()
441+
self.adc_mask = SettingsManager(device_info["device_name"]).get_adc_mask()
443442
if len(SettingsManager(device_info["device_name"]).get_channel_names()) == len(self.adc_mask):
444443
self.channel_names = SettingsManager(device_info["device_name"]).get_channel_names()
445444
else:
@@ -544,64 +543,92 @@ def __init__(self, device_info, calib_param, notch_freq):
544543
self.packet_buffer = []
545544

546545
def _add_filters(self):
547-
bp_freq = self._device_info['sampling_rate'] / 4 - \
548-
1.5, self._device_info['sampling_rate'] / 4 + 1.5
549-
noise_freq = self._device_info['sampling_rate'] / \
550-
4 + 2.5, self._device_info['sampling_rate'] / 4 + 5.5
546+
s_rate = 250
547+
center = s_rate / 4
548+
549+
bp_freq = (center - 1.5, center + 1.5)
550+
noise_freq = (center + 2.5, center + 5.5)
551+
551552
settings_manager = SettingsManager(self._device_info["device_name"])
552-
settings_manager.load_current_settings()
553-
n_chan = settings_manager.settings_dict[settings_manager.channel_count_key]
554-
# Temporary fix for 16/32 channel filters
555-
if not is_explore_pro_device():
556-
if n_chan >= 16:
557-
n_chan = 32
558-
self._filters['notch'] = ExGFilter(cutoff_freq=self._notch_freq,
559-
filter_type='notch_imp',
560-
s_rate=self._device_info['sampling_rate'],
561-
n_chan=n_chan)
562-
563-
self._filters['demodulation'] = ExGFilter(cutoff_freq=bp_freq,
564-
filter_type='bandpass',
565-
s_rate=self._device_info['sampling_rate'],
566-
n_chan=n_chan)
567-
568-
self._filters['base_noise'] = ExGFilter(cutoff_freq=noise_freq,
569-
filter_type='bandpass',
570-
s_rate=self._device_info['sampling_rate'],
571-
n_chan=n_chan)
553+
n_chan = settings_manager.get_channel_count()
554+
self.adc_count = n_chan // 8
555+
556+
def make_filter(filter_type, cutoff, channels):
557+
return ExGFilter(
558+
cutoff_freq=cutoff,
559+
filter_type=filter_type,
560+
s_rate=s_rate,
561+
n_chan=channels,
562+
)
563+
564+
if not self._calib_param['calibration']:
565+
566+
self._filters['notch'] = [
567+
make_filter('notch_imp', self._notch_freq, 8)
568+
for _ in range(self.adc_count)
569+
]
570+
self._filters['demodulation'] = [
571+
make_filter('bandpass', bp_freq, 8)
572+
for _ in range(self.adc_count)
573+
]
574+
self._filters['base_noise'] = [
575+
make_filter('bandpass', noise_freq, 8)
576+
for _ in range(self.adc_count)
577+
]
578+
else:
579+
self._filters['notch'] = make_filter('notch_imp', self._notch_freq, n_chan)
580+
self._filters['demodulation'] = make_filter('bandpass', bp_freq, n_chan)
581+
self._filters['base_noise'] = make_filter('bandpass', noise_freq, n_chan)
572582

573583
def measure_imp(self, packet):
574584
"""Compute electrode impedances
575585
"""
586+
if self._calib_param['calibration']:
587+
return None
576588
self.packet_buffer.append(packet)
577589

578590
if len(self.packet_buffer) < 16:
579591
return None
580592
else:
581-
timestamp, _ = self.packet_buffer[0].get_data()
593+
timestamp, data = self.packet_buffer[0].get_data()
582594
resized_packet = BleImpedancePacket(
583595
timestamp=timestamp, payload=None)
584596
resized_packet.populate_packet_with_data(self.packet_buffer)
585597
self.packet_buffer.clear()
586-
temp_packet = self._filters['notch'].apply(
587-
input_data=resized_packet, in_place=False)
588-
self._calib_param['noise_level'] = self._filters['base_noise']. \
589-
apply(input_data=temp_packet, in_place=False).get_ptp()
590-
self._filters['demodulation'].apply(
591-
input_data=temp_packet, in_place=True
592-
).calculate_impedance(self._calib_param)
593-
return temp_packet
598+
imp_packet_buffer = []
599+
if isinstance(self._filters['notch'], list):
600+
temp_packet = None
601+
# slice packet and feed it to the filters
602+
for i in range(self.adc_count):
603+
sliced_packet = BleImpedancePacket(
604+
timestamp=timestamp, payload=None)
605+
sliced_packet.resize_packet(resized_packet.get_data()[1], i)
606+
temp_packet = self._filters['notch'][i].apply(
607+
input_data=sliced_packet, in_place=False)
608+
self._calib_param['noise_level'] = self._filters['base_noise'][i]. \
609+
apply(input_data=temp_packet, in_place=False).get_ptp()
610+
self._filters['demodulation'][i].apply(
611+
input_data=temp_packet, in_place=True
612+
)
613+
temp_packet.calculate_impedance(self._calib_param, index=i)
614+
temp_packet.data = temp_packet.imp_data
615+
imp_packet_buffer.append(temp_packet)
616+
imp_packet = BleImpedancePacket(timestamp=timestamp, payload=None)
617+
imp_packet.populate_data_1d(imp_packet_buffer)
618+
imp_packet.imp_data = imp_packet.data
619+
620+
return imp_packet
594621

595622

596623
def find_free_port():
597-
"""Find a free port on the localhost
624+
"""Find a free port on the localhost)
625+
free_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
598626
599627
Returns:
600628
int: Port number
601629
"""
602630
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as free_socket:
603631
free_socket.bind(('localhost', 0))
604-
free_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
605632
port_number = free_socket.getsockname()[1]
606633
return port_number
607634

0 commit comments

Comments
 (0)