Skip to content

Commit bbe1cf5

Browse files
authored
Merge pull request #415 from Mentalab-hub/APIS-1071-we-want-to-work-on-explore-desktop-without-having-to-connect-to-a-physical-device
Apis 1071 we want to work on explore desktop without having to connect to a physical device
2 parents 672bd39 + 95faf80 commit bbe1cf5

3 files changed

Lines changed: 218 additions & 2 deletions

File tree

src/explorepy/__init__.py

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

2525
this = sys.modules[__name__]
2626
# TODO appropriate library
27-
bt_interface_list = ['sdk', 'ble', 'mock', 'pyserial', 'usb']
27+
bt_interface_list = ['sdk', 'ble', 'mock', 'pyserial', 'usb', 'csv']
2828
this._bt_interface = 'ble'
2929

3030
if not sys.version_info >= (3, 6):

src/explorepy/csv_client.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import time
2+
from enum import Enum, auto
3+
import numpy as np
4+
from explorepy.packet import BleImpedancePacket, DeviceInfoBLE, OrientationV1, OrientationV2
5+
from pylsl import local_clock
6+
from typing_extensions import override
7+
8+
9+
class ClientState(Enum):
10+
DISCONNECTED = auto()
11+
CONNECTED = auto()
12+
STREAMING = auto()
13+
STOPPED = auto()
14+
15+
class PacketSize(Enum):
16+
EEG_8 = 40
17+
EEG_32 = 112
18+
ORN = 50
19+
DEVICE_INFO = 38
20+
21+
class CsvClient:
22+
def __init__(self, channel_count):
23+
file_path = "../../explorepy/tests/sample_data/"
24+
file_name = "test_" + str(channel_count) + ".csv"
25+
self.server = server = CsvServer(
26+
channel_count=channel_count,
27+
csv_path=file_path + file_name,
28+
loop=True
29+
)
30+
self._state = ClientState.DISCONNECTED
31+
32+
def set_state(self, state: ClientState):
33+
if not isinstance(state, ClientState):
34+
raise ValueError("Invalid client state")
35+
self._state = state
36+
37+
def get_state(self) -> ClientState:
38+
return self._state
39+
40+
def connect(self):
41+
if self._state != ClientState.DISCONNECTED:
42+
return False
43+
self.set_state(ClientState.CONNECTED)
44+
return True
45+
46+
def disconnect(self):
47+
self.set_state(ClientState.DISCONNECTED)
48+
return True
49+
50+
def start_streaming(self):
51+
if self._state != ClientState.CONNECTED:
52+
raise RuntimeError(f"Cannot start streaming from state {self._state}")
53+
self.set_state(ClientState.STREAMING)
54+
55+
def stop_streaming(self):
56+
if self._state == ClientState.STREAMING:
57+
self.set_state(ClientState.STOPPED)
58+
59+
def read(self):
60+
if self._state != ClientState.STREAMING:
61+
if self._state == ClientState.CONNECTED:
62+
self.set_state(ClientState.STREAMING)
63+
device_info_packet = DeviceInfoMock(timestamp=self.server.ts, payload=None)
64+
device_info_packet.packet_size = PacketSize.DEVICE_INFO
65+
device_info_packet.set_info(self.server.device_info)
66+
return device_info_packet
67+
68+
69+
self.server.tick += 1
70+
if self.server.tick % 5 == 0:
71+
orn_packet = OrientationMock(timestamp=self.server.ts, payload=None)
72+
orn_packet.set_data(self.server.orn_value)
73+
orn_packet.packet_size = PacketSize.ORN
74+
return orn_packet
75+
76+
self.server.ts += self.server.time_period
77+
sleep_time = self.server.ts - local_clock()
78+
if sleep_time > 0:
79+
time.sleep(sleep_time)
80+
eeg_packet = BleImpedancePacket(timestamp=self.server.ts, payload=None)
81+
try:
82+
eeg_packet.data = self.server.read_sample()
83+
except StopIteration:
84+
self.set_state(ClientState.STOPPED)
85+
return None
86+
eeg_packet.packet_size = self.server.packet_size
87+
return eeg_packet
88+
89+
def write(self, bytes):
90+
pass
91+
92+
import numpy as np
93+
from pylsl import local_clock
94+
95+
96+
class CsvServer:
97+
def __init__(self, channel_count: int, csv_path: str, loop: bool = True):
98+
if channel_count not in (8, 32):
99+
raise ValueError("Only 8 or 32 channels supported")
100+
101+
self.channel_count = channel_count
102+
self.loop = loop
103+
self.csv_data = np.loadtxt(csv_path, delimiter=',', skiprows=1) # skip row 0
104+
105+
self.csv_data = self.csv_data[:, 1:]
106+
if self.csv_data.shape[1] != channel_count:
107+
print('######################', self.csv_data.shape)
108+
raise ValueError(
109+
f"CSV has {self.csv_data.shape[1]} columns, "
110+
f"expected {channel_count}"
111+
)
112+
113+
self.row_idx = 0
114+
self.num_rows = self.csv_data.shape[0]
115+
self.device_info_ble_32ch = {
116+
'device_name': 'Explore_DABD',
117+
'firmware_version': '9.6.9',
118+
'adc_mask': [1] * 8,
119+
'sampling_rate': 250,
120+
'is_imp_mode': False,
121+
'board_id': 'PCB_304_801p2_X',
122+
'memory_info': 1,
123+
'max_online_sps': 250,
124+
'max_offline_sps': 2000
125+
}
126+
127+
self.device_info_ble_8ch = {
128+
'device_name': 'Explore_AAAQ',
129+
'firmware_version': '7.6.9',
130+
'adc_mask': [1] * 8,
131+
'sampling_rate': 250,
132+
'is_imp_mode': False,
133+
'board_id': 'PCB_303_801E_XX',
134+
'memory_info': 1,
135+
'max_online_sps': 1000,
136+
'max_offline_sps': 8000
137+
}
138+
139+
self.device_info = (
140+
self.device_info_ble_8ch
141+
if channel_count == 8
142+
else self.device_info_ble_32ch
143+
)
144+
145+
self.fs = self.device_info['sampling_rate']
146+
self.time_period = np.round(1 / self.fs, 3)
147+
self.ts = local_clock()
148+
self.tick = 0
149+
150+
self.packet_size = (
151+
PacketSize.EEG_8 if channel_count == 8 else PacketSize.EEG_32
152+
)
153+
154+
self.orn_value = [
155+
5.002, -3.904, 1001.01, 420.0, -70.0, 210.0,
156+
103.36, 804.08, -532.0, -0.0023,
157+
-0.0028, 0.0371, 0.9993
158+
]
159+
160+
def read_sample(self):
161+
if self.row_idx >= self.num_rows:
162+
if not self.loop:
163+
raise StopIteration("End of CSV reached")
164+
self.row_idx = 0
165+
166+
sample = self.csv_data[self.row_idx]
167+
self.row_idx += 1
168+
169+
return sample.reshape(self.channel_count, 1)
170+
171+
def read_device_info(self):
172+
return self.device_info
173+
174+
class DeviceInfoMock(DeviceInfoBLE):
175+
def __init__(self, timestamp, payload, time_offset=0):
176+
self.timestamp = timestamp
177+
178+
def _convert(self, bin_data):
179+
pass
180+
181+
def set_info(self, info):
182+
self.info = info
183+
184+
def get_info(self):
185+
return self.info
186+
187+
188+
class OrientationMock(OrientationV2):
189+
"""Orientation data packet"""
190+
191+
def __init__(self, timestamp, payload, time_offset=0):
192+
self.timestamp = timestamp
193+
194+
def _convert(self, bin_data):
195+
pass
196+
197+
def get_data(self, srate=None):
198+
return [self.timestamp], self.data
199+
200+
def set_data(self, data):
201+
self.data = data

src/explorepy/parser.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
2+
import os
23
"""Parser module"""
34
import asyncio
45
import binascii
@@ -67,6 +68,7 @@ def __init__(self, callback, progress_callback=None, mode='device', debug=True):
6768
self.total_packet_size_read = 0
6869
self.progress = 0
6970
self.progress_callback = progress_callback
71+
self._generate_packet_impl = None
7072
self.header_len = 0
7173
self.data_len = 0
7274

@@ -79,6 +81,11 @@ def start_streaming(self, device_name, mac_address):
7981
elif explorepy.get_bt_interface() == 'mock':
8082
from explorepy.bt_mock_client import MockBtClient
8183
self.stream_interface = MockBtClient(device_name=device_name, mac_address=mac_address)
84+
elif explorepy.get_bt_interface() == 'csv':
85+
from explorepy.csv_client import CsvClient
86+
device_id = str.split(device_name, '_')[1] # split by underscore and take second part
87+
ch_count = 32 if device_id[0] == 'D' else 8 #dynamic channel selection based on first character
88+
self.stream_interface = CsvClient(ch_count)
8289
elif is_usb_mode():
8390
from explorepy.serial_client import SerialStream
8491
self.stream_interface = SerialStream(device_name=device_name)
@@ -87,6 +94,10 @@ def start_streaming(self, device_name, mac_address):
8794
"Please use the following command to use ExplorePy with a legacy device\n"
8895
"pip install explorepy==3.2.1\n"
8996
"https://explorepy.readthedocs.io/en/latest/explore_legacy_devices\n")
97+
if explorepy.get_bt_interface() == 'csv':
98+
self._generate_packet_impl = self._generate_packet_from_csv
99+
else:
100+
self._generate_packet_impl = self._generate_packet
90101
self.stream_interface.connect()
91102
self._stream()
92103

@@ -159,7 +170,7 @@ def _stream_loop(self):
159170
asyncio.set_event_loop(asyncio.new_event_loop())
160171
while self._do_streaming:
161172
try:
162-
packet, packet_size = self._generate_packet()
173+
packet, packet_size = self._generate_packet_impl()
163174
self.total_packet_size_read += packet_size
164175
self.callback(packet=packet)
165176
except ReconnectionFlowError:
@@ -369,6 +380,10 @@ def get_header_bytes(self):
369380
else:
370381
return self.stream_interface.read(self.header_len)
371382

383+
def _generate_packet_from_csv(self):
384+
packet = self.stream_interface.read()
385+
return packet, packet.packet_size.value
386+
372387

373388
class FileHandler:
374389
"""Binary file handler with conditional memory mapping for improved performance"""

0 commit comments

Comments
 (0)