Skip to content

Changes in UI and 3 different protocols-USB, WIFI, BLE added #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
47fa087
NPG-Wifi Code
PayalLakra Apr 10, 2025
3001385
Connection establsihed with LSL, CSV for WIFI,USB,BLE in different files
PayalLakra Apr 15, 2025
f1e0d44
Rename files
PayalLakra Apr 16, 2025
1da72f4
Remove commented code that of no use
PayalLakra Apr 17, 2025
49a18f0
Adding Closing options as well
PayalLakra Apr 17, 2025
7dd68ad
Adding cleanup statements
PayalLakra Apr 17, 2025
dc4d3b4
Check if stream is closed or not sending the data(for more than 2 sec…
PayalLakra Apr 17, 2025
c528fe3
Application Closes when no data received upto 2 seconds.(LSL Disconne…
PayalLakra Apr 18, 2025
2caa5da
Handle LSL stream disconnection by closing app after 2s of no data
PayalLakra Apr 18, 2025
6796cd9
Handle LSL stream disconnection by closing app after 2s of no data
PayalLakra Apr 18, 2025
c9a457b
Handle LSL stream disconnection by closing app after 2s of no data.
PayalLakra Apr 18, 2025
96724df
Handle LSL stream disconnection by closing app after 2s of no data.
PayalLakra Apr 18, 2025
ed35af3
Handle LSL stream disconnection by closing app after 2s of no data.
PayalLakra Apr 18, 2025
c247a91
Changing the name of the class
PayalLakra Apr 18, 2025
6811ba5
Change the name of the class
PayalLakra Apr 18, 2025
6b3d3e1
Class name changed
PayalLakra Apr 18, 2025
a2d87ab
Well connected for wifi,usb but issues with BLE.
PayalLakra May 1, 2025
1bd152a
Worked - need some changes like UI, Disconnect button
PayalLakra May 2, 2025
f550b54
Merge branch 'main' of https://github.com/upsidedownlabs/Chords-Pytho…
PayalLakra May 2, 2025
617e87e
Connect button, csv button is added
PayalLakra May 2, 2025
9571eec
UI Changes fixed
PayalLakra May 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
555 changes: 166 additions & 389 deletions app.py

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion beetle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pylsl
import numpy as np
import time
import sys
from pylsl import StreamInlet, resolve_streams, resolve_byprop
from scipy.signal import iirnotch, butter, lfilter
import math
Expand All @@ -12,8 +13,13 @@
streams = resolve_streams()
available_streams = [s.name() for s in streams]

last_data_time = None
stream_active = True
inlet = None # Initialize inlet variable

if not available_streams:
print("No LSL streams found!")
sys.exit(1)

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

if inlet is None:
print("Could not connect to any stream.")
sys.exit(1)

sampling_rate = int(inlet.info().nominal_srate())
print(f"Sampling rate: {sampling_rate} Hz")

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

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

# Apply filters
def apply_filters(eeg_point):
filtered = lfilter(b_notch, a_notch, [eeg_point])
Expand Down Expand Up @@ -97,7 +110,7 @@ def calculate_focus_level(eeg_data, sampling_rate=500):
pygame.display.set_caption('Beetle Game')

def calibrate():
global focus_threshold
global focus_threshold, last_data_time
calibration_data = []

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

sample, _ = inlet.pull_sample(timeout=0.1)
if sample:
last_data_time = time.time()
filtered_sample = apply_filters(sample[0])
calibration_data.append(filtered_sample)
else:
if last_data_time and (time.time() - last_data_time) > 2:
show_message("Connection lost! Exiting...", 2)
pygame.quit()
sys.exit(1)

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

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

sample, _ = inlet.pull_sample(timeout=0.1)
if sample:
last_data_time = time.time()
filtered_sample = apply_filters(sample[0])
buffer.append(filtered_sample)
else:
if last_data_time and (time.time() - last_data_time) > 2:
show_message("Connection lost! Exiting...", 2)
running = False
break

current_time = time.time()

Expand Down
371 changes: 0 additions & 371 deletions chords.py

This file was deleted.

207 changes: 207 additions & 0 deletions chords_ble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import asyncio
from bleak import BleakScanner, BleakClient
import time
import sys
import argparse
import threading

class Chords_BLE:
# Class constants
DEVICE_NAME_PREFIX = "NPG"
SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"
CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"

# Packet parameters
SINGLE_SAMPLE_LEN = 7 # (1 Counter + 3 Channels * 2 bytes)
BLOCK_COUNT = 10
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT

def __init__(self):
self.prev_unrolled_counter = None
self.samples_received = 0
self.start_time = None
self.total_missing_samples = 0
self.last_received_time = None
self.DATA_TIMEOUT = 2.0
self.client = None
self.monitor_task = None
self.print_rate_task = None
self.running = False
self.loop = None
self.connection_event = threading.Event()
self.stop_event = threading.Event()

@classmethod
async def scan_devices(cls):
print("Scanning for BLE devices...")
devices = await BleakScanner.discover()
filtered = [d for d in devices if d.name and d.name.startswith(cls.DEVICE_NAME_PREFIX)]

if not filtered:
print("No NPG devices found.")
return []

return filtered

def process_sample(self, sample_data: bytearray):
"""Process a single EEG sample packet"""
self.last_received_time = time.time()

if len(sample_data) != self.SINGLE_SAMPLE_LEN:
print("Unexpected sample length:", len(sample_data))
return

sample_counter = sample_data[0]
if self.prev_unrolled_counter is None:
self.prev_unrolled_counter = sample_counter
else:
last = self.prev_unrolled_counter % 256
if sample_counter < last:
current_unrolled = self.prev_unrolled_counter - last + sample_counter + 256
else:
current_unrolled = self.prev_unrolled_counter - last + sample_counter

if current_unrolled != self.prev_unrolled_counter + 1:
missing = current_unrolled - (self.prev_unrolled_counter + 1)
print(f"Missing {missing} sample(s)")
self.total_missing_samples += missing

self.prev_unrolled_counter = current_unrolled

if self.start_time is None:
self.start_time = time.time()

channels = [
int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]

self.samples_received += 1

def notification_handler(self, sender, data: bytearray):
"""Handle incoming notifications from the BLE device"""
try:
if len(data) == self.NEW_PACKET_LEN:
for i in range(0, self.NEW_PACKET_LEN, self.SINGLE_SAMPLE_LEN):
self.process_sample(data[i:i+self.SINGLE_SAMPLE_LEN])
elif len(data) == self.SINGLE_SAMPLE_LEN:
self.process_sample(data)
else:
print(f"Unexpected packet length: {len(data)} bytes")
except Exception as e:
print(f"Error processing data: {e}")

async def print_rate(self):
while not self.stop_event.is_set():
await asyncio.sleep(1)
self.samples_received = 0

async def monitor_connection(self):
"""Monitor the connection status and check for data interruptions"""
while not self.stop_event.is_set():
if self.last_received_time and (time.time() - self.last_received_time) > self.DATA_TIMEOUT:
print("\nData Interrupted")
print("Cleanup Completed.")
self.running = False
break
if self.client and not self.client.is_connected:
print("\nData Interrupted (Bluetooth disconnected)")
print("Cleanup Completed.")
self.running = False
break
await asyncio.sleep(0.5)

async def async_connect(self, device_address):
try:
print(f"Attempting to connect to {device_address}...")

self.client = BleakClient(device_address)
await self.client.connect()

if not self.client.is_connected:
print("Failed to connect")
return False

print(f"Connected to {device_address}", flush=True)
self.connection_event.set()

self.last_received_time = time.time()
self.monitor_task = asyncio.create_task(self.monitor_connection())
self.print_rate_task = asyncio.create_task(self.print_rate())

await self.client.write_gatt_char(self.CONTROL_CHAR_UUID, b"START", response=True)
print("Sent START command")

await self.client.start_notify(self.DATA_CHAR_UUID, self.notification_handler)
print("Subscribed to data notifications")

self.running = True
while self.running and not self.stop_event.is_set():
await asyncio.sleep(1)

return True

except Exception as e:
print(f"Connection error: {str(e)}")
return False
finally:
await self.cleanup()

async def cleanup(self):
if self.monitor_task:
self.monitor_task.cancel()
if self.print_rate_task:
self.print_rate_task.cancel()
if self.client and self.client.is_connected:
await self.client.disconnect()
self.running = False
self.connection_event.clear()

def connect(self, device_address):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)

try:
self.loop.run_until_complete(self.async_connect(device_address))
except Exception as e:
print(f"Error in connection: {str(e)}")
return False
finally:
if self.loop.is_running():
self.loop.close()

def stop(self):
self.stop_event.set()
self.running = False
if self.loop and self.loop.is_running():
self.loop.call_soon_threadsafe(self.loop.stop)

def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--scan", action="store_true", help="Scan for devices")
parser.add_argument("--connect", type=str, help="Connect to device address")
return parser.parse_args()

if __name__ == "__main__":
args = parse_args()
client = Chords_BLE()

try:
if args.scan:
devices = asyncio.run(Chords_BLE.scan_devices())
for dev in devices:
print(f"DEVICE:{dev.name}|{dev.address}")
elif args.connect:
client.connect(args.connect)
try:
while client.running:
time.sleep(1)
except KeyboardInterrupt:
client.stop()
else:
print("Please specify --scan or --connect")
sys.exit(1)
except Exception as e:
print(f"Error: {str(e)}")
sys.exit(1)
Loading