1
+ import asyncio
2
+ from bleak import BleakScanner , BleakClient
3
+ import time
4
+ from pylsl import StreamInfo , StreamOutlet
5
+
6
+ # BLE parameters (must match your firmware)
7
+ DEVICE_NAME = "NPG-30:30:f9:f9:db:76"
8
+ SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
9
+ DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"
10
+ CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"
11
+
12
+ # Packet parameters for batched samples:
13
+ SINGLE_SAMPLE_LEN = 7 # Each sample is 7 bytes
14
+ BLOCK_COUNT = 10 # Batch size: 10 samples per notification
15
+ NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT # Total packet length (70 bytes)
16
+
17
+ # Set up an LSL stream with int16 data format (irregular rate)
18
+ stream_name = "NPG"
19
+ info = StreamInfo (stream_name , "EXG" , 3 , 500 , "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
26
+ total_missing_samples = 0
27
+
28
+ def process_sample (sample_data : bytearray ):
29
+ global prev_unrolled_counter , samples_received , start_time , total_missing_samples
30
+ if len (sample_data ) != SINGLE_SAMPLE_LEN :
31
+ print ("Unexpected sample length:" , len (sample_data ))
32
+ return
33
+ sample_counter = sample_data [0 ]
34
+ # Unroll the counter:
35
+ if prev_unrolled_counter is None :
36
+ prev_unrolled_counter = sample_counter
37
+ else :
38
+ last = prev_unrolled_counter % 256
39
+ if sample_counter < last :
40
+ current_unrolled = prev_unrolled_counter - last + sample_counter + 256
41
+ else :
42
+ current_unrolled = prev_unrolled_counter - last + sample_counter
43
+ if current_unrolled != prev_unrolled_counter + 1 :
44
+ print (f"Missing sample: expected { prev_unrolled_counter + 1 } , got { current_unrolled } " )
45
+ total_missing_samples += current_unrolled - (prev_unrolled_counter + 1 )
46
+ prev_unrolled_counter = current_unrolled
47
+
48
+ # Set start_time when first sample is received
49
+ if start_time is None :
50
+ start_time = time .time ()
51
+ elapsed = time .time () - start_time
52
+
53
+ channels = []
54
+ for ch in range (3 ):
55
+ offset = 1 + ch * 2
56
+ value = int .from_bytes (sample_data [offset :offset + 2 ], byteorder = 'big' , signed = True )
57
+ channels .append (value )
58
+ print (f"Sample { prev_unrolled_counter } at { elapsed :.2f} s: Channels: { channels } Total missing samples: { total_missing_samples } " )
59
+ outlet .push_sample (channels )
60
+ samples_received += 1
61
+
62
+ def notification_handler (sender , data : bytearray ):
63
+ if len (data ) == NEW_PACKET_LEN :
64
+ for i in range (0 , NEW_PACKET_LEN , SINGLE_SAMPLE_LEN ):
65
+ sample = data [i :i + SINGLE_SAMPLE_LEN ]
66
+ process_sample (sample )
67
+ elif len (data ) == SINGLE_SAMPLE_LEN :
68
+ process_sample (data )
69
+ else :
70
+ print ("Unexpected packet length:" , len (data ))
71
+
72
+ async def print_rate ():
73
+ global samples_received
74
+ while True :
75
+ await asyncio .sleep (1 )
76
+ print (f"Samples per second: { samples_received } " )
77
+ samples_received = 0
78
+
79
+ async def run ():
80
+ print ("Scanning for BLE devices with name starting with" , DEVICE_NAME )
81
+ devices = await BleakScanner .discover ()
82
+ target = None
83
+ for d in devices :
84
+ if d .name and DEVICE_NAME .lower () in d .name .lower ():
85
+ target = d
86
+ break
87
+ if target is None :
88
+ print ("No target device found" )
89
+ return
90
+
91
+ print ("Connecting to:" , target .name , target .address )
92
+ async with BleakClient (target ) as client :
93
+ if not client .is_connected :
94
+ print ("Failed to connect" )
95
+ return
96
+ print ("Connected to" , target .name )
97
+ await client .write_gatt_char (CONTROL_CHAR_UUID , b"START" , response = True )
98
+ print ("Sent START command" )
99
+ await client .start_notify (DATA_CHAR_UUID , notification_handler )
100
+ print ("Subscribed to data notifications" )
101
+ asyncio .create_task (print_rate ())
102
+ while True :
103
+ await asyncio .sleep (1 )
104
+
105
+ if __name__ == "__main__" :
106
+ asyncio .run (run ())
0 commit comments