Skip to content

Commit 34ba1a8

Browse files
committed
Fully Documented Code
1 parent 75a6d25 commit 34ba1a8

File tree

4 files changed

+662
-319
lines changed

4 files changed

+662
-319
lines changed

chordspy/chords_ble.py

Lines changed: 91 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
This scripts scan and then connects to the selected devices via BLE, reads data packets, processes them, and handles connection status.
3+
"""
4+
5+
# Importing necessary libraries
16
import asyncio
27
from bleak import BleakScanner, BleakClient
38
import time
@@ -6,38 +11,56 @@
611
import threading
712

813
class Chords_BLE:
14+
"""
15+
A class to handle BLE communication with NPG devices via BLE.
16+
This class provides functionality to:
17+
- Scan for compatible BLE devices
18+
- Connect to a device
19+
- Receive and process data packets
20+
- Monitor connection status
21+
- Handle disconnections and errors
22+
"""
23+
924
# 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"
25+
DEVICE_NAME_PREFIX = "NPG" # Prefix for compatible device names
26+
SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b" # UUID for the BLE service
27+
DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8" # UUID for data characteristic
28+
CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb" # UUID for control characteristic
1429

1530
# Packet parameters
16-
NUM_CHANNELS = 3
17-
SINGLE_SAMPLE_LEN = (NUM_CHANNELS * 2) + 1 # (1 Counter + Num_Channels * 2 bytes)
31+
NUM_CHANNELS = 3 # Number of channels
32+
SINGLE_SAMPLE_LEN = (NUM_CHANNELS * 2) + 1 # (1 Counter + Num_Channels * 2 bytes)
1833
BLOCK_COUNT = 10
19-
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT
34+
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT # Total length of a data packet
2035

2136
def __init__(self):
22-
self.prev_unrolled_counter = None
23-
self.samples_received = 0
24-
self.start_time = None
25-
self.total_missing_samples = 0
26-
self.last_received_time = None
27-
self.DATA_TIMEOUT = 2.0
28-
self.client = None
29-
self.monitor_task = None
30-
self.print_rate_task = None
31-
self.running = False
32-
self.loop = None
33-
self.connection_event = threading.Event()
34-
self.stop_event = threading.Event()
37+
"""
38+
Initialize the BLE client with default values and state variables.
39+
"""
40+
self.prev_unrolled_counter = None # Tracks the last sample counter value
41+
self.samples_received = 0 # Count of received samples
42+
self.start_time = None # Timestamp when first sample is received
43+
self.total_missing_samples = 0 # Count of missing samples
44+
self.last_received_time = None # Timestamp of last received data
45+
self.DATA_TIMEOUT = 2.0 # Timeout period for data reception (seconds)
46+
self.client = None # BLE client instance
47+
self.monitor_task = None # Task for monitoring connection
48+
self.print_rate_task = None # Task for printing sample rate
49+
self.running = False # Flag indicating if client is running
50+
self.loop = None # Asyncio event loop
51+
self.connection_event = threading.Event() # Event for connection status
52+
self.stop_event = threading.Event() # Event for stopping operations
3553

3654
@classmethod
3755
async def scan_devices(cls):
56+
"""
57+
Scan for BLE devices with the NPG prefix.
58+
Returns:
59+
list: A list of discovered devices matching the NPG prefix
60+
"""
3861
print("Scanning for BLE devices...")
3962
devices = await BleakScanner.discover()
40-
filtered = [d for d in devices if d.name and d.name.startswith(cls.DEVICE_NAME_PREFIX)]
63+
filtered = [d for d in devices if d.name and d.name.startswith(cls.DEVICE_NAME_PREFIX)] # Filter devices by name prefix
4164

4265
if not filtered:
4366
print("No NPG devices found.")
@@ -46,13 +69,19 @@ async def scan_devices(cls):
4669
return filtered
4770

4871
def process_sample(self, sample_data: bytearray):
49-
"""Process a single EEG sample packet"""
72+
"""
73+
Process a single sample packet.
74+
Args:
75+
sample_data (bytearray): The raw sample data to process
76+
"""
5077
self.last_received_time = time.time()
5178

79+
# Validate sample length
5280
if len(sample_data) != self.SINGLE_SAMPLE_LEN:
5381
print("Unexpected sample length:", len(sample_data))
5482
return
5583

84+
# Extract and process sample counter
5685
sample_counter = sample_data[0]
5786
if self.prev_unrolled_counter is None:
5887
self.prev_unrolled_counter = sample_counter
@@ -63,41 +92,55 @@ def process_sample(self, sample_data: bytearray):
6392
else:
6493
current_unrolled = self.prev_unrolled_counter - last + sample_counter
6594

95+
# Check for missing samples
6696
if current_unrolled != self.prev_unrolled_counter + 1:
6797
missing = current_unrolled - (self.prev_unrolled_counter + 1)
6898
print(f"Missing {missing} sample(s)")
6999
self.total_missing_samples += missing
70100

71101
self.prev_unrolled_counter = current_unrolled
72102

103+
# Record start time if this is the first sample
73104
if self.start_time is None:
74105
self.start_time = time.time()
75106

107+
# Extract channel data (2 bytes per channel, big-endian, signed)
76108
channels = [int.from_bytes(sample_data[i:i+2], byteorder='big', signed=True)
77109
for i in range(1, len(sample_data), 2)]
78110

79111
self.samples_received += 1
80112

81113
def notification_handler(self, sender, data: bytearray):
82-
"""Handle incoming notifications from the BLE device"""
114+
"""
115+
Handle incoming notifications from the BLE device.
116+
Args:
117+
sender: The characteristic that sent the notification
118+
data (bytearray): The received data packet
119+
"""
83120
try:
84-
if len(data) == self.NEW_PACKET_LEN:
85-
for i in range(0, self.NEW_PACKET_LEN, self.SINGLE_SAMPLE_LEN):
121+
if len(data) == self.NEW_PACKET_LEN: # Process data based on packet length
122+
for i in range(0, self.NEW_PACKET_LEN, self.SINGLE_SAMPLE_LEN): # Process a block of samples
86123
self.process_sample(data[i:i+self.SINGLE_SAMPLE_LEN])
87124
elif len(data) == self.SINGLE_SAMPLE_LEN:
88-
self.process_sample(data)
125+
self.process_sample(data) # Process a single sample
89126
else:
90127
print(f"Unexpected packet length: {len(data)} bytes")
91128
except Exception as e:
92129
print(f"Error processing data: {e}")
93130

94131
async def print_rate(self):
132+
"""Print the current sample rate every second."""
95133
while not self.stop_event.is_set():
96134
await asyncio.sleep(1)
97135
self.samples_received = 0
98136

99137
async def monitor_connection(self):
100-
"""Monitor the connection status and check for data interruptions"""
138+
"""
139+
Monitor the connection status and check for data interruptions.
140+
This runs in a loop to check:
141+
- If data hasn't been received within the timeout period
142+
- If the BLE connection has been lost
143+
"""
101144
while not self.stop_event.is_set():
102145
if self.last_received_time and (time.time() - self.last_received_time) > self.DATA_TIMEOUT:
103146
print("\nData Interrupted")
@@ -112,6 +155,13 @@ async def monitor_connection(self):
112155
await asyncio.sleep(0.5)
113156

114157
async def async_connect(self, device_address):
158+
"""
159+
Asynchronously connect to a BLE device and start data reception.
160+
Args:
161+
device_address (str): The MAC address of the device to connect to
162+
Returns:
163+
bool: True if connection was successful, otherwise False
164+
"""
115165
try:
116166
print(f"Attempting to connect to {device_address}...")
117167

@@ -125,16 +175,20 @@ async def async_connect(self, device_address):
125175
print(f"Connected to {device_address}", flush=True)
126176
self.connection_event.set()
127177

178+
# Initialize monitoring tasks
128179
self.last_received_time = time.time()
129180
self.monitor_task = asyncio.create_task(self.monitor_connection())
130181
self.print_rate_task = asyncio.create_task(self.print_rate())
131182

183+
# Send start command to device
132184
await self.client.write_gatt_char(self.CONTROL_CHAR_UUID, b"START", response=True)
133185
print("Sent START command")
134186

187+
# Subscribe to data notifications
135188
await self.client.start_notify(self.DATA_CHAR_UUID, self.notification_handler)
136189
print("Subscribed to data notifications")
137190

191+
# Main loop
138192
self.running = True
139193
while self.running and not self.stop_event.is_set():
140194
await asyncio.sleep(1)
@@ -148,6 +202,7 @@ async def async_connect(self, device_address):
148202
await self.cleanup()
149203

150204
async def cleanup(self):
205+
"""Clean up resources and disconnect from the device."""
151206
if self.monitor_task:
152207
self.monitor_task.cancel()
153208
if self.print_rate_task:
@@ -158,6 +213,11 @@ async def cleanup(self):
158213
self.connection_event.clear()
159214

160215
def connect(self, device_address):
216+
"""
217+
Connect to a BLE device (wrapper for async_connect).
218+
Args:
219+
device_address (str): The MAC address of the device to connect to
220+
"""
161221
self.loop = asyncio.new_event_loop()
162222
asyncio.set_event_loop(self.loop)
163223

@@ -171,12 +231,14 @@ def connect(self, device_address):
171231
self.loop.close()
172232

173233
def stop(self):
234+
"""Stop all operations and clean up resources."""
174235
self.stop_event.set()
175236
self.running = False
176237
if self.loop and self.loop.is_running():
177238
self.loop.call_soon_threadsafe(self.loop.stop)
178239

179240
def parse_args():
241+
"""Parse command line arguments."""
180242
parser = argparse.ArgumentParser()
181243
parser.add_argument("--scan", action="store_true", help="Scan for devices")
182244
parser.add_argument("--connect", type=str, help="Connect to device address")
@@ -188,11 +250,11 @@ def parse_args():
188250

189251
try:
190252
if args.scan:
191-
devices = asyncio.run(Chords_BLE.scan_devices())
253+
devices = asyncio.run(Chords_BLE.scan_devices()) # Scan for devices
192254
for dev in devices:
193255
print(f"DEVICE:{dev.name}|{dev.address}")
194256
elif args.connect:
195-
client.connect(args.connect)
257+
client.connect(args.connect) # Connect to specified device
196258
try:
197259
while client.running:
198260
time.sleep(1)

0 commit comments

Comments
 (0)