Skip to content

Commit 389e3af

Browse files
Adding BioListener boards support (#759)
* initial version of BioListener communication over tcp
1 parent 2ed99c6 commit 389e3af

File tree

18 files changed

+957
-22
lines changed

18 files changed

+957
-22
lines changed

.github/workflows/run_unix.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ jobs:
251251
run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1
252252
- name: Streaming Python Markers
253253
run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/markers.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1
254+
- name: BioListener Python
255+
run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/biolistener_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345
254256
- name: Denoising Python
255257
run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/denoising.py
256258
- name: Serialization Python

.github/workflows/run_windows.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ jobs:
186186
- name: KnightBoard Windows Python Test
187187
run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\knightboard_windows.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 57 --serial-port
188188
shell: cmd
189+
- name: BioListener Windows Python Test
190+
run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345
191+
shell: cmd
189192
# Signal Processing Testing
190193
- name: Serialization Rust Test
191194
run: |

csharp_package/brainflow/brainflow/board_controller_library.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ public enum BoardIds
122122
OB5000_8_CHANNELS_BOARD = 60,
123123
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61,
124124
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62,
125-
125+
OB3000_24_CHANNELS_BOARD = 63,
126+
BIOLISTENER_BOARD = 64,
126127
};
127128

128129

docs/SupportedBoards.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,3 +1347,47 @@ Supported platforms:
13471347
- Linux
13481348
- MacOS
13491349
- Devices like Raspberry Pi
1350+
1351+
1352+
BioListener
1353+
--------
1354+
1355+
BioListener
1356+
~~~~~~~~~~~~~
1357+
1358+
.. image:: https://live.staticflickr.com/65535/54273076343_6a7eb99697_k.jpg
1359+
:width: 519px
1360+
:height: 389px
1361+
1362+
`BioListener website <https://github.com/serhii-matsyshyn/biolistener/>`_
1363+
1364+
To create such board you need to specify the following board ID and fields of BrainFlowInputParams object:
1365+
1366+
- :code:`BoardIds.BIOLISTENER_BOARD`
1367+
- *optional:* :code:`ip_address`, ip address of the machine running the BrainFlow server (not the end device). If not provided, the server will listen on all network interfaces (at `0.0.0.0`)
1368+
- *optional:* :code:`ip_port`, any free local port. If the chosen port is in use, the next available free port will be used. If not provided, the search for a free port starts at `12345`
1369+
- *optional:* :code:`timeout`, timeout for device discovery, default is 3sec
1370+
1371+
Make sure to configure the BioListener board as stated in the `BioListener documentation <https://github.com/serhii-matsyshyn/biolistener/>`_ to connect to the BrainFlow server.
1372+
1373+
Initialization Example:
1374+
1375+
.. code-block:: python
1376+
1377+
params = BrainFlowInputParams()
1378+
params.ip_port = 12345
1379+
params.ip_address = "0.0.0.0"
1380+
board = BoardShim(BoardIds.BIOLISTENER_BOARD, params)
1381+
1382+
Supported platforms:
1383+
1384+
- Windows
1385+
- Linux
1386+
- MacOS
1387+
- Devices like Raspberry Pi
1388+
- Android
1389+
1390+
Available :ref:`presets-label`:
1391+
1392+
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG (EMG, ECG, EOG) data
1393+
- :code:`BrainFlowPresets.AUXILIARY_PRESET`, it contains Gyro, Accel, battery and ESP32 chip temperature data
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import datetime
2+
import enum
3+
import json
4+
import logging
5+
import random
6+
import socket
7+
import struct
8+
import subprocess
9+
import sys
10+
import threading
11+
import time
12+
13+
from brainflow_emulator.emulate_common import TestFailureError, log_multilines
14+
15+
BIOLISTENER_DATA_CHANNELS_COUNT = 8
16+
17+
BIOLISTENER_DATA_PACKET_DEBUG = 0
18+
BIOLISTENER_DATA_PACKET_BIOSIGNALS = 1
19+
BIOLISTENER_DATA_PACKET_IMU = 2
20+
21+
ADC_USED = 0 # ADS131M08
22+
23+
24+
class DataPacket:
25+
FORMAT_STRING = f'=1B 1I 1B 1I 1B {BIOLISTENER_DATA_CHANNELS_COUNT}I 1B'
26+
27+
def __init__(self, ts, tp, n, s_id, data):
28+
self.header = 0xA0
29+
self.ts = ts
30+
self.type = tp
31+
self.n = n
32+
self.s_id = s_id
33+
self.data = data
34+
self.footer = 0xC0
35+
36+
def pack(self):
37+
return struct.pack(self.FORMAT_STRING, self.header, self.ts, self.type, self.n, self.s_id, *self.data, self.footer)
38+
39+
@classmethod
40+
def unpack(cls, packed_data):
41+
format_string = cls.FORMAT_STRING
42+
unpacked_data = struct.unpack(format_string, packed_data)
43+
return cls(*unpacked_data)
44+
45+
def __repr__(self):
46+
return (f'DataPacket(header={self.header}, ts={self.ts}, type={self.type}, '
47+
f'n={self.n}, s_id={self.s_id}, data={self.data}, footer={self.footer})')
48+
49+
50+
class State(enum.Enum):
51+
wait = 'wait'
52+
stream = 'stream'
53+
54+
55+
def test_socket(cmd_list):
56+
logging.info('Running %s' % ' '.join([str(x) for x in cmd_list]))
57+
process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
58+
stdout, stderr = process.communicate()
59+
60+
log_multilines(logging.info, stdout)
61+
log_multilines(logging.info, stderr)
62+
63+
if process.returncode != 0:
64+
raise TestFailureError('Test failed with exit code %s' % str(process.returncode), process.returncode)
65+
66+
return stdout, stderr
67+
68+
69+
def run_socket_server():
70+
thread = BioListenerEmulator()
71+
thread.start()
72+
return thread
73+
74+
75+
class BioListenerEmulator(threading.Thread):
76+
77+
def __init__(self):
78+
threading.Thread.__init__(self)
79+
self.local_ip = '127.0.0.1'
80+
self.local_port = 12345
81+
self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
82+
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
83+
self.server_socket.settimeout(1)
84+
self.state = State.wait.value
85+
self.package_num = 0
86+
self.keep_alive = True
87+
self.connection_established = False
88+
logging.info(f"BioListener emulator started")
89+
90+
@staticmethod
91+
def volts_to_data(ref, voltage, pga_gain, adc_resolution):
92+
resolution = ref / (adc_resolution * pga_gain)
93+
94+
if voltage >= 0: # Positive range
95+
raw_code = voltage / resolution
96+
else: # Negative range
97+
raw_code = (voltage + (ref / pga_gain)) / resolution
98+
99+
raw_code = int(raw_code)
100+
raw_code = max(0, min(0xFFFFFF, raw_code)) # Ensure within 24 bit range
101+
102+
return raw_code
103+
104+
def run(self):
105+
logging.info(f"BioListener emulator connecting to {self.local_ip}:{self.local_port}...")
106+
while self.keep_alive and not self.connection_established:
107+
try:
108+
self.server_socket.connect((self.local_ip, self.local_port))
109+
self.connection_established = True
110+
break
111+
except Exception as err:
112+
logging.warning(f"Error connecting to {self.local_ip}:{self.local_port}: {err}")
113+
# A failed connect may leave the socket unusable.
114+
try:
115+
self.server_socket.close()
116+
except Exception:
117+
pass
118+
# Recreate the socket with the same options.
119+
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
120+
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
121+
self.server_socket.settimeout(1)
122+
time.sleep(0.1)
123+
124+
if self.connection_established:
125+
logging.info(f"BioListener emulator connected to {self.local_ip}:{self.local_port}")
126+
else:
127+
logging.error(f"BioListener emulator failed to connect to {self.local_ip}:{self.local_port}")
128+
return
129+
130+
started_at = time.time()
131+
while self.keep_alive:
132+
new_data_packet = DataPacket(
133+
ts=int((time.time() - started_at) * 1000),
134+
tp=BIOLISTENER_DATA_PACKET_BIOSIGNALS,
135+
n=self.package_num,
136+
s_id=ADC_USED,
137+
data=[
138+
self.volts_to_data(
139+
ref=2500000.0,
140+
voltage=random.uniform(-1000, 1000),
141+
pga_gain=8,
142+
adc_resolution=16777216.0
143+
) for _ in range(BIOLISTENER_DATA_CHANNELS_COUNT)
144+
]
145+
)
146+
self.package_num += 1
147+
148+
try:
149+
data = self.server_socket.recv(1024)
150+
message = data.decode('utf-8').strip()
151+
152+
if message:
153+
for message_part in message.split("\n"):
154+
logging.info(f"BioListener received command: {message_part}")
155+
json_str = json.loads(message_part)
156+
if json_str["command"] in (1, 2, 3, 4):
157+
logging.info("Command ignored - simulator supports only start and stop stream command")
158+
elif json_str["command"] == 5:
159+
logging.info("Start stream command received")
160+
self.state = State.stream.value
161+
elif json_str["command"] == 6:
162+
logging.info("Stop stream command received")
163+
self.state = State.wait.value
164+
else:
165+
logging.warning(f"Unknown command: {json_str['command']}")
166+
except TimeoutError:
167+
pass
168+
except socket.timeout:
169+
pass
170+
except Exception as err:
171+
logging.error(f"Error in recv thread: {err}")
172+
173+
try:
174+
if self.state == State.stream.value:
175+
self.server_socket.sendall(new_data_packet.pack())
176+
except ConnectionResetError:
177+
logging.error(f"Connection lost")
178+
except Exception as e:
179+
logging.error(f"Error: {e}")
180+
181+
182+
def main(cmd_list):
183+
if not cmd_list:
184+
raise Exception('No command to execute')
185+
server_thread = run_socket_server()
186+
187+
try:
188+
test_socket(cmd_list)
189+
finally:
190+
server_thread.keep_alive = False
191+
server_thread.join()
192+
193+
194+
if __name__ == '__main__':
195+
logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO)
196+
main(sys.argv[1:])

java_package/brainflow/src/main/java/brainflow/BoardIds.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ public enum BoardIds
7171
SYNCHRONI_OCTO_8_CHANNELS_BOARD(59),
7272
OB5000_8_CHANNELS_BOARD(60),
7373
SYNCHRONI_PENTO_8_CHANNELS_BOARD(61),
74-
SYNCHRONI_UNO_1_CHANNELS_BOARD(62);
75-
74+
SYNCHRONI_UNO_1_CHANNELS_BOARD(62),
75+
OB3000_24_CHANNELS_BOARD(63),
76+
BIOLISTENER_BOARD(64);
7677

7778
private final int board_id;
7879
private static final Map<Integer, BoardIds> bi_map = new HashMap<Integer, BoardIds> ();

julia_package/brainflow/src/board_shim.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,14 @@ export BrainFlowInputParams
6161
EXPLORE_PLUS_8_CHAN_BOARD = 54
6262
EXPLORE_PLUS_32_CHAN_BOARD = 55
6363
PIEEG_BOARD = 56
64-
6564
NEUROPAWN_KNIGHT_BOARD = 57
6665
SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58
6766
SYNCHRONI_OCTO_8_CHANNELS_BOARD = 59
6867
OB5000_8_CHANNELS_BOARD = 60
6968
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61
7069
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62
71-
72-
70+
OB3000_24_CHANNELS_BOARD = 63
71+
BIOLISTENER_BOARD = 64
7372

7473
end
7574

matlab_package/brainflow/BoardIds.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@
5959
EXPLORE_PLUS_8_CHAN_BOARD(54)
6060
EXPLORE_PLUS_32_CHAN_BOARD(55)
6161
PIEEG_BOARD(56)
62-
6362
NEUROPAWN_KNIGHT_BOARD(57)
6463
SYNCHRONI_TRIO_3_CHANNELS_BOARD(58)
6564
SYNCHRONI_OCTO_8_CHANNELS_BOARD(59)
6665
OB5000_8_CHANNELS_BOARD(60)
6766
SYNCHRONI_PENTO_8_CHANNELS_BOARD(61)
6867
SYNCHRONI_UNO_1_CHANNELS_BOARD(62)
69-
68+
OB3000_24_CHANNELS_BOARD(63)
69+
BIOLISTENER_BOARD(64)
7070
end
7171
end

nodejs_package/brainflow/brainflow.types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,16 @@ export enum BoardIds {
6969
EXPLORE_PLUS_8_CHAN_BOARD = 54,
7070
EXPLORE_PLUS_32_CHAN_BOARD = 55,
7171
PIEEG_BOARD = 56,
72-
7372
NEUROPAWN_KNIGHT_BOARD = 57,
7473
SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58,
7574
SYNCHRONI_OCTO_CHANNELS_BOARD = 59,
7675
OB5000_8_CHANNELS_BOARD = 60,
7776
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61,
78-
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62
79-
77+
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62,
78+
OB3000_24_CHANNELS_BOARD = 63,
79+
BIOLISTENER_BOARD = 64
8080
}
81+
8182
export enum IpProtocolTypes {
8283
NO_IP_PROTOCOL = 0,
8384
UDP = 1,

python_package/brainflow/board_shim.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ class BoardIds(enum.IntEnum):
8080
OB5000_8_CHANNELS_BOARD = 60 #:
8181
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 #:
8282
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 #:
83-
83+
OB3000_24_CHANNELS_BOARD = 63 #:
84+
BIOLISTENER_BOARD = 64 #:
8485

8586

8687
class IpProtocolTypes(enum.IntEnum):

0 commit comments

Comments
 (0)