Skip to content

Commit c71ffe0

Browse files
authored
Merge pull request #40 from PayalLakra/new
Updated UI with different protocols-USB, WIFI, BLE added
2 parents 0b503e6 + 46a5185 commit c71ffe0

19 files changed

+2078
-1717
lines changed

app.py

Lines changed: 196 additions & 388 deletions
Large diffs are not rendered by default.

beetle.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pylsl
33
import numpy as np
44
import time
5+
import sys
56
from pylsl import StreamInlet, resolve_streams, resolve_byprop
67
from scipy.signal import iirnotch, butter, lfilter
78
import math
@@ -12,8 +13,13 @@
1213
streams = resolve_streams()
1314
available_streams = [s.name() for s in streams]
1415

16+
last_data_time = None
17+
stream_active = True
18+
inlet = None # Initialize inlet variable
19+
1520
if not available_streams:
1621
print("No LSL streams found!")
22+
sys.exit(1)
1723

1824
for stream_name in available_streams:
1925
print(f"Trying to connect to {stream_name}...")
@@ -28,6 +34,8 @@
2834

2935
if inlet is None:
3036
print("Could not connect to any stream.")
37+
sys.exit(1)
38+
3139
sampling_rate = int(inlet.info().nominal_srate())
3240
print(f"Sampling rate: {sampling_rate} Hz")
3341

@@ -70,6 +78,11 @@ def show_message(message, duration=3):
7078
screen.blit(text, text_rect)
7179
pygame.display.update()
7280

81+
for event in pygame.event.get():
82+
if event.type == pygame.QUIT:
83+
pygame.quit()
84+
sys.exit()
85+
7386
# Apply filters
7487
def apply_filters(eeg_point):
7588
filtered = lfilter(b_notch, a_notch, [eeg_point])
@@ -97,7 +110,7 @@ def calculate_focus_level(eeg_data, sampling_rate=500):
97110
pygame.display.set_caption('Beetle Game')
98111

99112
def calibrate():
100-
global focus_threshold
113+
global focus_threshold, last_data_time
101114
calibration_data = []
102115

103116
font = pygame.font.SysFont("Arial", 36, bold=True)
@@ -119,8 +132,19 @@ def calibrate():
119132

120133
sample, _ = inlet.pull_sample(timeout=0.1)
121134
if sample:
135+
last_data_time = time.time()
122136
filtered_sample = apply_filters(sample[0])
123137
calibration_data.append(filtered_sample)
138+
else:
139+
if last_data_time and (time.time() - last_data_time) > 2:
140+
show_message("Connection lost! Exiting...", 2)
141+
pygame.quit()
142+
sys.exit(1)
143+
144+
for event in pygame.event.get():
145+
if event.type == pygame.QUIT:
146+
pygame.quit()
147+
sys.exit()
124148

125149
if len(calibration_data) >= buffer_size: # Ensure enough data was collected
126150
eeg_data = np.array(calibration_data)
@@ -170,8 +194,14 @@ def update_beetle_position(focus_level, is_focus_stable):
170194

171195
sample, _ = inlet.pull_sample(timeout=0.1)
172196
if sample:
197+
last_data_time = time.time()
173198
filtered_sample = apply_filters(sample[0])
174199
buffer.append(filtered_sample)
200+
else:
201+
if last_data_time and (time.time() - last_data_time) > 2:
202+
show_message("Connection lost! Exiting...", 2)
203+
running = False
204+
break
175205

176206
current_time = time.time()
177207

chords.py

Lines changed: 0 additions & 371 deletions
This file was deleted.

chords_ble.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import asyncio
2+
from bleak import BleakScanner, BleakClient
3+
import time
4+
import sys
5+
import argparse
6+
import threading
7+
8+
class Chords_BLE:
9+
# Class constants
10+
DEVICE_NAME_PREFIX = "NPG"
11+
SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
12+
DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"
13+
CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"
14+
15+
# Packet parameters
16+
SINGLE_SAMPLE_LEN = 7 # (1 Counter + 3 Channels * 2 bytes)
17+
BLOCK_COUNT = 10
18+
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT
19+
20+
def __init__(self):
21+
self.prev_unrolled_counter = None
22+
self.samples_received = 0
23+
self.start_time = None
24+
self.total_missing_samples = 0
25+
self.last_received_time = None
26+
self.DATA_TIMEOUT = 2.0
27+
self.client = None
28+
self.monitor_task = None
29+
self.print_rate_task = None
30+
self.running = False
31+
self.loop = None
32+
self.connection_event = threading.Event()
33+
self.stop_event = threading.Event()
34+
35+
@classmethod
36+
async def scan_devices(cls):
37+
print("Scanning for BLE devices...")
38+
devices = await BleakScanner.discover()
39+
filtered = [d for d in devices if d.name and d.name.startswith(cls.DEVICE_NAME_PREFIX)]
40+
41+
if not filtered:
42+
print("No NPG devices found.")
43+
return []
44+
45+
return filtered
46+
47+
def process_sample(self, sample_data: bytearray):
48+
"""Process a single EEG sample packet"""
49+
self.last_received_time = time.time()
50+
51+
if len(sample_data) != self.SINGLE_SAMPLE_LEN:
52+
print("Unexpected sample length:", len(sample_data))
53+
return
54+
55+
sample_counter = sample_data[0]
56+
if self.prev_unrolled_counter is None:
57+
self.prev_unrolled_counter = sample_counter
58+
else:
59+
last = self.prev_unrolled_counter % 256
60+
if sample_counter < last:
61+
current_unrolled = self.prev_unrolled_counter - last + sample_counter + 256
62+
else:
63+
current_unrolled = self.prev_unrolled_counter - last + sample_counter
64+
65+
if current_unrolled != self.prev_unrolled_counter + 1:
66+
missing = current_unrolled - (self.prev_unrolled_counter + 1)
67+
print(f"Missing {missing} sample(s)")
68+
self.total_missing_samples += missing
69+
70+
self.prev_unrolled_counter = current_unrolled
71+
72+
if self.start_time is None:
73+
self.start_time = time.time()
74+
75+
channels = [
76+
int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
77+
int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
78+
int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]
79+
80+
self.samples_received += 1
81+
82+
def notification_handler(self, sender, data: bytearray):
83+
"""Handle incoming notifications from the BLE device"""
84+
try:
85+
if len(data) == self.NEW_PACKET_LEN:
86+
for i in range(0, self.NEW_PACKET_LEN, self.SINGLE_SAMPLE_LEN):
87+
self.process_sample(data[i:i+self.SINGLE_SAMPLE_LEN])
88+
elif len(data) == self.SINGLE_SAMPLE_LEN:
89+
self.process_sample(data)
90+
else:
91+
print(f"Unexpected packet length: {len(data)} bytes")
92+
except Exception as e:
93+
print(f"Error processing data: {e}")
94+
95+
async def print_rate(self):
96+
while not self.stop_event.is_set():
97+
await asyncio.sleep(1)
98+
self.samples_received = 0
99+
100+
async def monitor_connection(self):
101+
"""Monitor the connection status and check for data interruptions"""
102+
while not self.stop_event.is_set():
103+
if self.last_received_time and (time.time() - self.last_received_time) > self.DATA_TIMEOUT:
104+
print("\nData Interrupted")
105+
print("Cleanup Completed.")
106+
self.running = False
107+
break
108+
if self.client and not self.client.is_connected:
109+
print("\nData Interrupted (Bluetooth disconnected)")
110+
print("Cleanup Completed.")
111+
self.running = False
112+
break
113+
await asyncio.sleep(0.5)
114+
115+
async def async_connect(self, device_address):
116+
try:
117+
print(f"Attempting to connect to {device_address}...")
118+
119+
self.client = BleakClient(device_address)
120+
await self.client.connect()
121+
122+
if not self.client.is_connected:
123+
print("Failed to connect")
124+
return False
125+
126+
print(f"Connected to {device_address}", flush=True)
127+
self.connection_event.set()
128+
129+
self.last_received_time = time.time()
130+
self.monitor_task = asyncio.create_task(self.monitor_connection())
131+
self.print_rate_task = asyncio.create_task(self.print_rate())
132+
133+
await self.client.write_gatt_char(self.CONTROL_CHAR_UUID, b"START", response=True)
134+
print("Sent START command")
135+
136+
await self.client.start_notify(self.DATA_CHAR_UUID, self.notification_handler)
137+
print("Subscribed to data notifications")
138+
139+
self.running = True
140+
while self.running and not self.stop_event.is_set():
141+
await asyncio.sleep(1)
142+
143+
return True
144+
145+
except Exception as e:
146+
print(f"Connection error: {str(e)}")
147+
return False
148+
finally:
149+
await self.cleanup()
150+
151+
async def cleanup(self):
152+
if self.monitor_task:
153+
self.monitor_task.cancel()
154+
if self.print_rate_task:
155+
self.print_rate_task.cancel()
156+
if self.client and self.client.is_connected:
157+
await self.client.disconnect()
158+
self.running = False
159+
self.connection_event.clear()
160+
161+
def connect(self, device_address):
162+
self.loop = asyncio.new_event_loop()
163+
asyncio.set_event_loop(self.loop)
164+
165+
try:
166+
self.loop.run_until_complete(self.async_connect(device_address))
167+
except Exception as e:
168+
print(f"Error in connection: {str(e)}")
169+
return False
170+
finally:
171+
if self.loop.is_running():
172+
self.loop.close()
173+
174+
def stop(self):
175+
self.stop_event.set()
176+
self.running = False
177+
if self.loop and self.loop.is_running():
178+
self.loop.call_soon_threadsafe(self.loop.stop)
179+
180+
def parse_args():
181+
parser = argparse.ArgumentParser()
182+
parser.add_argument("--scan", action="store_true", help="Scan for devices")
183+
parser.add_argument("--connect", type=str, help="Connect to device address")
184+
return parser.parse_args()
185+
186+
if __name__ == "__main__":
187+
args = parse_args()
188+
client = Chords_BLE()
189+
190+
try:
191+
if args.scan:
192+
devices = asyncio.run(Chords_BLE.scan_devices())
193+
for dev in devices:
194+
print(f"DEVICE:{dev.name}|{dev.address}")
195+
elif args.connect:
196+
client.connect(args.connect)
197+
try:
198+
while client.running:
199+
time.sleep(1)
200+
except KeyboardInterrupt:
201+
client.stop()
202+
else:
203+
print("Please specify --scan or --connect")
204+
sys.exit(1)
205+
except Exception as e:
206+
print(f"Error: {str(e)}")
207+
sys.exit(1)

0 commit comments

Comments
 (0)