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
1
6
import asyncio
2
7
from bleak import BleakScanner , BleakClient
3
8
import time
6
11
import threading
7
12
8
13
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
+
9
24
# 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
14
29
15
30
# 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)
18
33
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
20
35
21
36
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
35
53
36
54
@classmethod
37
55
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
+ """
38
61
print ("Scanning for BLE devices..." )
39
62
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
41
64
42
65
if not filtered :
43
66
print ("No NPG devices found." )
@@ -46,13 +69,19 @@ async def scan_devices(cls):
46
69
return filtered
47
70
48
71
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
+ """
50
77
self .last_received_time = time .time ()
51
78
79
+ # Validate sample length
52
80
if len (sample_data ) != self .SINGLE_SAMPLE_LEN :
53
81
print ("Unexpected sample length:" , len (sample_data ))
54
82
return
55
83
84
+ # Extract and process sample counter
56
85
sample_counter = sample_data [0 ]
57
86
if self .prev_unrolled_counter is None :
58
87
self .prev_unrolled_counter = sample_counter
@@ -63,41 +92,55 @@ def process_sample(self, sample_data: bytearray):
63
92
else :
64
93
current_unrolled = self .prev_unrolled_counter - last + sample_counter
65
94
95
+ # Check for missing samples
66
96
if current_unrolled != self .prev_unrolled_counter + 1 :
67
97
missing = current_unrolled - (self .prev_unrolled_counter + 1 )
68
98
print (f"Missing { missing } sample(s)" )
69
99
self .total_missing_samples += missing
70
100
71
101
self .prev_unrolled_counter = current_unrolled
72
102
103
+ # Record start time if this is the first sample
73
104
if self .start_time is None :
74
105
self .start_time = time .time ()
75
106
107
+ # Extract channel data (2 bytes per channel, big-endian, signed)
76
108
channels = [int .from_bytes (sample_data [i :i + 2 ], byteorder = 'big' , signed = True )
77
109
for i in range (1 , len (sample_data ), 2 )]
78
110
79
111
self .samples_received += 1
80
112
81
113
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
+ """
83
120
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
86
123
self .process_sample (data [i :i + self .SINGLE_SAMPLE_LEN ])
87
124
elif len (data ) == self .SINGLE_SAMPLE_LEN :
88
- self .process_sample (data )
125
+ self .process_sample (data ) # Process a single sample
89
126
else :
90
127
print (f"Unexpected packet length: { len (data )} bytes" )
91
128
except Exception as e :
92
129
print (f"Error processing data: { e } " )
93
130
94
131
async def print_rate (self ):
132
+ """Print the current sample rate every second."""
95
133
while not self .stop_event .is_set ():
96
134
await asyncio .sleep (1 )
97
135
self .samples_received = 0
98
136
99
137
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
+ """
101
144
while not self .stop_event .is_set ():
102
145
if self .last_received_time and (time .time () - self .last_received_time ) > self .DATA_TIMEOUT :
103
146
print ("\n Data Interrupted" )
@@ -112,6 +155,13 @@ async def monitor_connection(self):
112
155
await asyncio .sleep (0.5 )
113
156
114
157
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
+ """
115
165
try :
116
166
print (f"Attempting to connect to { device_address } ..." )
117
167
@@ -125,16 +175,20 @@ async def async_connect(self, device_address):
125
175
print (f"Connected to { device_address } " , flush = True )
126
176
self .connection_event .set ()
127
177
178
+ # Initialize monitoring tasks
128
179
self .last_received_time = time .time ()
129
180
self .monitor_task = asyncio .create_task (self .monitor_connection ())
130
181
self .print_rate_task = asyncio .create_task (self .print_rate ())
131
182
183
+ # Send start command to device
132
184
await self .client .write_gatt_char (self .CONTROL_CHAR_UUID , b"START" , response = True )
133
185
print ("Sent START command" )
134
186
187
+ # Subscribe to data notifications
135
188
await self .client .start_notify (self .DATA_CHAR_UUID , self .notification_handler )
136
189
print ("Subscribed to data notifications" )
137
190
191
+ # Main loop
138
192
self .running = True
139
193
while self .running and not self .stop_event .is_set ():
140
194
await asyncio .sleep (1 )
@@ -148,6 +202,7 @@ async def async_connect(self, device_address):
148
202
await self .cleanup ()
149
203
150
204
async def cleanup (self ):
205
+ """Clean up resources and disconnect from the device."""
151
206
if self .monitor_task :
152
207
self .monitor_task .cancel ()
153
208
if self .print_rate_task :
@@ -158,6 +213,11 @@ async def cleanup(self):
158
213
self .connection_event .clear ()
159
214
160
215
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
+ """
161
221
self .loop = asyncio .new_event_loop ()
162
222
asyncio .set_event_loop (self .loop )
163
223
@@ -171,12 +231,14 @@ def connect(self, device_address):
171
231
self .loop .close ()
172
232
173
233
def stop (self ):
234
+ """Stop all operations and clean up resources."""
174
235
self .stop_event .set ()
175
236
self .running = False
176
237
if self .loop and self .loop .is_running ():
177
238
self .loop .call_soon_threadsafe (self .loop .stop )
178
239
179
240
def parse_args ():
241
+ """Parse command line arguments."""
180
242
parser = argparse .ArgumentParser ()
181
243
parser .add_argument ("--scan" , action = "store_true" , help = "Scan for devices" )
182
244
parser .add_argument ("--connect" , type = str , help = "Connect to device address" )
@@ -188,11 +250,11 @@ def parse_args():
188
250
189
251
try :
190
252
if args .scan :
191
- devices = asyncio .run (Chords_BLE .scan_devices ())
253
+ devices = asyncio .run (Chords_BLE .scan_devices ()) # Scan for devices
192
254
for dev in devices :
193
255
print (f"DEVICE:{ dev .name } |{ dev .address } " )
194
256
elif args .connect :
195
- client .connect (args .connect )
257
+ client .connect (args .connect ) # Connect to specified device
196
258
try :
197
259
while client .running :
198
260
time .sleep (1 )
0 commit comments