2
2
from bleak import BleakScanner , BleakClient
3
3
import time
4
4
from pylsl import StreamInfo , StreamOutlet
5
+ import sys
6
+ import argparse
5
7
6
8
# BLE parameters (must match your firmware)
7
9
DEVICE_NAME_PREFIX = "NPG"
14
16
BLOCK_COUNT = 10 # Batch size: 10 samples per notification
15
17
NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT # Total packet length (70 bytes)
16
18
17
- # Set up an LSL stream with int16 data format (irregular rate)
18
- stream_name = "NPG"
19
- info = StreamInfo (stream_name , "EXG" , 3 , 0 , "int16" , "uid007" )
20
- outlet = StreamOutlet (info )
21
-
22
- # Global variables for unrolled counter, sample counting, and timing
23
- prev_unrolled_counter = None # Unrolled (cumulative) counter from firmware
24
- samples_received = 0 # Total samples received in the last second
25
- start_time = None # Time when first sample is received
19
+ # Global variables
20
+ prev_unrolled_counter = None
21
+ samples_received = 0
22
+ start_time = None
26
23
total_missing_samples = 0
24
+ outlet = None
25
+
26
+ def parse_args ():
27
+ parser = argparse .ArgumentParser ()
28
+ parser .add_argument ("--scan" , action = "store_true" , help = "Scan for devices and print them" )
29
+ parser .add_argument ("--connect" , type = str , help = "Connect to a specific device address" )
30
+ return parser .parse_args ()
31
+
32
+ async def scan_devices ():
33
+ print ("Scanning for BLE devices..." , file = sys .stderr )
34
+ devices = await BleakScanner .discover ()
35
+ filtered = [d for d in devices if d .name and d .name .startswith (DEVICE_NAME_PREFIX )]
36
+
37
+ if not filtered :
38
+ print ("No devices found." , file = sys .stderr )
39
+ return
40
+
41
+ # Print devices in format that Flask can parse
42
+ for dev in filtered :
43
+ print (f"DEVICE:{ dev .name } |{ dev .address } " )
27
44
28
45
def process_sample (sample_data : bytearray ):
29
- global prev_unrolled_counter , samples_received , start_time , total_missing_samples
46
+ global prev_unrolled_counter , samples_received , start_time , total_missing_samples , outlet
47
+
30
48
if len (sample_data ) != SINGLE_SAMPLE_LEN :
31
49
print ("Unexpected sample length:" , len (sample_data ))
32
50
return
33
- # Expected sample format:
34
- # Byte0: Packet counter (0-255)
35
- # Byte1-2: Channel 0 data (big-endian)
36
- # Byte3-4: Channel 1 data (big-endian)
37
- # Byte5-6: Channel 2 data (big-endian)
51
+
38
52
sample_counter = sample_data [0 ]
39
53
# Unroll the counter:
40
54
if prev_unrolled_counter is None :
@@ -45,76 +59,97 @@ def process_sample(sample_data: bytearray):
45
59
current_unrolled = prev_unrolled_counter - last + sample_counter + 256
46
60
else :
47
61
current_unrolled = prev_unrolled_counter - last + sample_counter
62
+
48
63
if current_unrolled != prev_unrolled_counter + 1 :
49
- print (f"Missing sample: expected { prev_unrolled_counter + 1 } , got { current_unrolled } " )
50
- total_missing_samples += current_unrolled - (prev_unrolled_counter + 1 )
64
+ missing = current_unrolled - (prev_unrolled_counter + 1 )
65
+ print (f"Missing { missing } sample(s): expected { prev_unrolled_counter + 1 } , got { current_unrolled } " )
66
+ total_missing_samples += missing
67
+
51
68
prev_unrolled_counter = current_unrolled
52
69
53
70
# Set start_time when first sample is received
54
71
if start_time is None :
55
72
start_time = time .time ()
56
- elapsed = time .time () - start_time
57
-
58
- channels = []
59
- for ch in range (3 ):
60
- offset = 1 + ch * 2
61
- value = int .from_bytes (sample_data [offset :offset + 2 ], byteorder = 'big' , signed = True )
62
- channels .append (value )
63
- print (f"Sample { prev_unrolled_counter } at { elapsed :.2f} s: Channels: { channels } Total missing samples: { total_missing_samples } " )
64
- outlet .push_sample (channels )
73
+
74
+ # Process channels
75
+ channels = [
76
+ int .from_bytes (sample_data [1 :3 ]), # Channel 0
77
+ int .from_bytes (sample_data [3 :5 ]), # Channel 1
78
+ int .from_bytes (sample_data [5 :7 ])]
79
+
80
+ # Push to LSL
81
+ if outlet :
82
+ outlet .push_sample (channels )
83
+
65
84
samples_received += 1
85
+
86
+ # Periodic status print
87
+ if samples_received % 100 == 0 :
88
+ elapsed = time .time () - start_time
89
+ print (f"Sample { prev_unrolled_counter } at { elapsed :.2f} s - Channels: { channels } - Missing: { total_missing_samples } " )
66
90
67
91
def notification_handler (sender , data : bytearray ):
68
- if len (data ) == NEW_PACKET_LEN :
69
- for i in range (0 , NEW_PACKET_LEN , SINGLE_SAMPLE_LEN ):
70
- sample = data [i :i + SINGLE_SAMPLE_LEN ]
71
- process_sample (sample )
72
- elif len (data ) == SINGLE_SAMPLE_LEN :
73
- process_sample (data )
74
- else :
75
- print ("Unexpected packet length:" , len (data ))
76
-
77
- async def print_rate ():
78
- global samples_received
79
- while True :
80
- await asyncio .sleep (1 )
81
- print (f"Samples per second: { samples_received } " )
82
- samples_received = 0
83
-
84
- async def run ():
85
- print ("Scanning for BLE devices with name starting with" , DEVICE_NAME_PREFIX )
86
- devices = await BleakScanner .discover ()
87
- # Filter devices with names starting with the prefix
88
- filtered = [d for d in devices if d .name and d .name .lower ().startswith (DEVICE_NAME_PREFIX .lower ())]
89
- if not filtered :
90
- print ("No devices found." )
91
- return
92
+ try :
93
+ if len (data ) == NEW_PACKET_LEN :
94
+ # Process batched samples
95
+ for i in range (0 , NEW_PACKET_LEN , SINGLE_SAMPLE_LEN ):
96
+ process_sample (data [i :i + SINGLE_SAMPLE_LEN ])
97
+ elif len (data ) == SINGLE_SAMPLE_LEN :
98
+ # Process single sample
99
+ process_sample (data )
100
+ else :
101
+ print (f"Unexpected packet length: { len (data )} bytes" )
102
+ except Exception as e :
103
+ print (f"Error processing data: { e } " )
92
104
93
- print ("Found devices:" )
94
- for idx , dev in enumerate (filtered ):
95
- print (f"{ idx } : { dev .name } ({ dev .address } )" )
105
+ async def connect_to_device (device_address ):
106
+ global outlet
107
+
108
+ print (f"Attempting to connect to { device_address } ..." , file = sys .stderr )
96
109
97
- # Let the user choose which device to connect to
110
+ # Set up LSL stream (500Hz sampling rate)
111
+ info = StreamInfo ("NPG" , "EXG" , 3 , 500 , "int16" , "npg1234" )
112
+ outlet = StreamOutlet (info )
113
+
114
+ client = None
98
115
try :
99
- choice = int (input ("Enter the index of the device to connect: " ))
100
- target = filtered [choice ]
101
- except (ValueError , IndexError ):
102
- print ("Invalid selection." )
103
- return
104
-
105
- print ("Connecting to:" , target .name , target .address )
106
- async with BleakClient (target ) as client :
116
+ client = BleakClient (device_address )
117
+ await client .connect ()
118
+
107
119
if not client .is_connected :
108
- print ("Failed to connect" )
109
- return
110
- print ("Connected to" , target .name )
120
+ print ("Failed to connect" , file = sys .stderr )
121
+ return False
122
+
123
+ print (f"Connected to { device_address } " )
124
+
125
+ # Send start command
111
126
await client .write_gatt_char (CONTROL_CHAR_UUID , b"START" , response = True )
112
127
print ("Sent START command" )
128
+
129
+ # Subscribe to notifications
113
130
await client .start_notify (DATA_CHAR_UUID , notification_handler )
114
131
print ("Subscribed to data notifications" )
115
- asyncio .create_task (print_rate ())
116
- while True :
132
+
133
+ # Keep connection alive
134
+ while client .is_connected :
117
135
await asyncio .sleep (1 )
136
+
137
+ return True
138
+
139
+ except Exception as e :
140
+ print (f"Connection error: { str (e )} " , file = sys .stderr )
141
+ return False
142
+ finally :
143
+ if client and client .is_connected :
144
+ await client .disconnect ()
118
145
119
146
if __name__ == "__main__" :
120
- asyncio .run (run ())
147
+ args = parse_args ()
148
+
149
+ if args .scan :
150
+ asyncio .run (scan_devices ())
151
+ elif args .connect :
152
+ asyncio .run (connect_to_device (args .connect ))
153
+ else :
154
+ print ("Please specify --scan or --connect" , file = sys .stderr )
155
+ sys .exit (1 )
0 commit comments