Skip to content

Commit b3a702b

Browse files
committed
Adding comments
1 parent c1ebe03 commit b3a702b

File tree

1 file changed

+71
-54
lines changed

1 file changed

+71
-54
lines changed

npg-ble.py

Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,69 +13,79 @@
1313
CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"
1414

1515
# Packet parameters
16-
SINGLE_SAMPLE_LEN = 7
16+
SINGLE_SAMPLE_LEN = 7 # (1 Counter + 3 Channels * 2 bytes)
1717
BLOCK_COUNT = 10
1818
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT
1919

2020
class NPGBluetoothClient:
2121
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.outlet = None
27-
self.last_received_time = None
28-
self.DATA_TIMEOUT = 2.0
29-
self.client = None
30-
self.monitor_task = None
31-
self.print_rate_task = None
32-
self.running = False
33-
self.loop = None
34-
self.connection_event = threading.Event()
35-
self.stop_event = threading.Event()
22+
self.prev_unrolled_counter = None # Previous Counter
23+
self.samples_received = 0 # Number of Samples received
24+
self.start_time = None # Start time of the first sample
25+
self.total_missing_samples = 0 # Total missing samples
26+
self.outlet = None # LSL outlet
27+
self.last_received_time = None # Last time a sample was received
28+
self.DATA_TIMEOUT = 2.0 # Timeout for considering data interrupted
29+
self.client = None # Bleak client instance
30+
self.monitor_task = None # Task for monitoring connection
31+
self.print_rate_task = None # Task for printing sample rate
32+
self.running = False # Flag indicating if NPGBluetoothClient is running or not
33+
self.loop = None # Event loop for asyncio
34+
self.connection_event = threading.Event() # Event for connection status
35+
self.stop_event = threading.Event() # Event for stopping all operations
3636

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

41+
# Validate Sample Length
4042
if len(sample_data) != SINGLE_SAMPLE_LEN:
4143
print("Unexpected sample length:", len(sample_data))
4244
return
43-
45+
46+
# Extract and Validate Sample Counter
4447
sample_counter = sample_data[0]
4548
if self.prev_unrolled_counter is None:
4649
self.prev_unrolled_counter = sample_counter
4750
else:
51+
# Calculate unrolled counter (handling 0-255)
4852
last = self.prev_unrolled_counter % 256
4953
if sample_counter < last:
5054
current_unrolled = self.prev_unrolled_counter - last + sample_counter + 256
5155
else:
5256
current_unrolled = self.prev_unrolled_counter - last + sample_counter
5357

58+
# Check for missing samples
5459
if current_unrolled != self.prev_unrolled_counter + 1:
5560
missing = current_unrolled - (self.prev_unrolled_counter + 1)
5661
print(f"Missing {missing} sample(s)")
5762
self.total_missing_samples += missing
5863

5964
self.prev_unrolled_counter = current_unrolled
60-
65+
66+
# Initialize timing on first sample received
6167
if self.start_time is None:
6268
self.start_time = time.time()
6369

70+
# Extract 3 channels of EEG data (16-bit signed integers, big-endian)
6471
channels = [
6572
int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
6673
int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
6774
int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]
6875

76+
# Push sample to LSL outlet
6977
if self.outlet:
7078
self.outlet.push_sample(channels)
7179

7280
self.samples_received += 1
7381

82+
# Periodically print the number of samples received and the elapsed time when 500 samples are received
7483
if self.samples_received % 500 == 0:
7584
elapsed = time.time() - self.start_time
7685
print(f"Received {self.samples_received} samples in {elapsed:.2f}s")
7786

7887
def notification_handler(self, sender, data: bytearray):
88+
"""Handle incoming notifications from the BLE device"""
7989
try:
8090
if len(data) == NEW_PACKET_LEN:
8191
for i in range(0, NEW_PACKET_LEN, SINGLE_SAMPLE_LEN):
@@ -88,44 +98,46 @@ def notification_handler(self, sender, data: bytearray):
8898
print(f"Error processing data: {e}")
8999

90100
async def print_rate(self):
91-
while not self.stop_event.is_set():
101+
"""Periodically print the sample rate every second"""
102+
while not self.stop_event.is_set(): # Continue running until stop event is triggered
92103
await asyncio.sleep(1)
93104
print(f"Samples per second: {self.samples_received}")
94-
self.samples_received = 0
105+
self.samples_received = 0 # Reset the counter after printing
95106

96107
async def monitor_connection(self):
97-
while not self.stop_event.is_set():
98-
if self.last_received_time and (time.time() - self.last_received_time) > self.DATA_TIMEOUT:
108+
"""Monitor the connection status and check for data interruptions"""
109+
while not self.stop_event.is_set(): # Continue running until stop event is triggered
110+
if self.last_received_time and (time.time() - self.last_received_time) > self.DATA_TIMEOUT: # Check for Data Timeout
99111
print("\nData Interrupted")
100112
self.running = False
101113
break
102-
if self.client and not self.client.is_connected:
114+
if self.client and not self.client.is_connected: # Check for BLE Disconnection
103115
print("\nData Interrupted (Bluetooth disconnected)")
104-
self.running = False
105-
break
106-
await asyncio.sleep(0.5)
116+
self.running = False # Set running flag to False
117+
break # Exit the monitoring loop
118+
await asyncio.sleep(0.5) # Short sleep to prevent busy-waiting
107119

108120
async def async_connect(self, device_address):
121+
"""Asynchronous function to establish BLE connection and start data streaming"""
109122
try:
110123
print(f"Attempting to connect to {device_address}...")
111124

112-
# Set up LSL stream
113-
info = StreamInfo("NPG", "EXG", 3, 500, "int16", "npg1234")
114-
self.outlet = StreamOutlet(info)
125+
info = StreamInfo("NPG", "EXG", 3, 500, "int16", "npg1234") # Set up LSL stream
126+
self.outlet = StreamOutlet(info) # Create the LSL output stream
115127

116-
self.client = BleakClient(device_address)
117-
await self.client.connect()
128+
self.client = BleakClient(device_address) # Initialize and connect BLE client using the device address
129+
await self.client.connect() # Asynchronously connect to the BLE device
118130

119-
if not self.client.is_connected:
131+
if not self.client.is_connected: # Verify connection was successful
120132
print("Failed to connect")
121-
return False
133+
return False # Return False if connection failed
122134

123135
print(f"Connected to {device_address}", flush=True)
124-
self.connection_event.set()
136+
self.connection_event.set() # Shows connection is established
125137

126-
self.last_received_time = time.time()
127-
self.monitor_task = asyncio.create_task(self.monitor_connection())
128-
self.print_rate_task = asyncio.create_task(self.print_rate())
138+
self.last_received_time = time.time() # Record current time as last received
139+
self.monitor_task = asyncio.create_task(self.monitor_connection()) # Task to monitor connection status
140+
self.print_rate_task = asyncio.create_task(self.print_rate()) # Task to periodically print sample rate
129141

130142
# Send start command
131143
await self.client.write_gatt_char(CONTROL_CHAR_UUID, b"START", response=True)
@@ -135,6 +147,7 @@ async def async_connect(self, device_address):
135147
await self.client.start_notify(DATA_CHAR_UUID, self.notification_handler)
136148
print("Subscribed to data notifications")
137149

150+
# Main processing loop
138151
self.running = True
139152
while self.running and not self.stop_event.is_set():
140153
await asyncio.sleep(1)
@@ -148,62 +161,66 @@ async def async_connect(self, device_address):
148161
await self.cleanup()
149162

150163
async def cleanup(self):
164+
"""Clean up resources and disconnect from the BLE device"""
151165
if self.monitor_task:
152-
self.monitor_task.cancel()
166+
self.monitor_task.cancel() # Cancel the background monitoring task if it exists
153167
if self.print_rate_task:
154-
self.print_rate_task.cancel()
168+
self.print_rate_task.cancel() # Cancel the sample rate printing task if it exists
155169
if self.client and self.client.is_connected:
156-
await self.client.disconnect()
157-
self.running = False
158-
self.connection_event.clear()
170+
await self.client.disconnect() # Disconnect from the BLE device if currently connected
171+
self.running = False # Set running flag to False
172+
self.connection_event.clear() # Clear the connection event flag
159173

160174
def connect(self, device_address):
161-
self.loop = asyncio.new_event_loop()
162-
asyncio.set_event_loop(self.loop)
175+
self.loop = asyncio.new_event_loop() # Create a new async event loop (required for async operations)
176+
asyncio.set_event_loop(self.loop) # Set this as the active loop for our thread
163177

164178
try:
165-
self.loop.run_until_complete(self.async_connect(device_address))
179+
self.loop.run_until_complete(self.async_connect(device_address)) # Run the async connection until it finishes
166180
except Exception as e:
167-
print(f"Error in connection: {str(e)}")
181+
print(f"Error in connection: {str(e)}") # If connection fails, print error and return False
168182
return False
169183
finally:
170-
if self.loop.is_running():
184+
if self.loop.is_running(): # Always clean up by closing the loop when everything is done
171185
self.loop.close()
172186

173187
def stop(self):
188+
"""Stop all operations and clean up"""
174189
self.stop_event.set()
175190
self.running = False
176191
if self.loop and self.loop.is_running():
177192
self.loop.call_soon_threadsafe(self.loop.stop)
178193

179194
def parse_args():
195+
"""Parse command line arguments"""
180196
parser = argparse.ArgumentParser()
181197
parser.add_argument("--scan", action="store_true", help="Scan for devices")
182198
parser.add_argument("--connect", type=str, help="Connect to device address")
183199
return parser.parse_args()
184200

185201
async def scan_devices():
202+
"""Scan for BLE devices with NPG prefix"""
186203
print("Scanning for BLE devices...")
187-
devices = await BleakScanner.discover()
188-
filtered = [d for d in devices if d.name and d.name.startswith(DEVICE_NAME_PREFIX)]
204+
devices = await BleakScanner.discover() # Discover all nearby BLE devices
205+
filtered = [d for d in devices if d.name and d.name.startswith(DEVICE_NAME_PREFIX)] # Filter devices to only those with matching name prefix
189206

190207
if not filtered:
191208
print("No devices found.")
192209
return
193210

194-
for dev in filtered:
211+
for dev in filtered: # Print each matching device's name and address
195212
print(f"DEVICE:{dev.name}|{dev.address}")
196213

197214
if __name__ == "__main__":
198-
args = parse_args()
199-
client = NPGBluetoothClient()
215+
args = parse_args() # Handle command line arguments
216+
client = NPGBluetoothClient() # Create Bluetooth client instance
200217

201218
try:
202-
if args.scan:
219+
if args.scan: # Scan flag - discover available devices
203220
asyncio.run(scan_devices())
204-
elif args.connect:
221+
elif args.connect: # Connect flag - connect to a specific device
205222
client.connect(args.connect)
206-
try:
223+
try: # Keep running until data interrupted or connection fails
207224
while client.running:
208225
time.sleep(1)
209226
except KeyboardInterrupt:

0 commit comments

Comments
 (0)