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_PREFIX = "NPG"
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
+ # 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)
38
+ sample_counter = sample_data[0]
39
+ # Unroll the counter:
40
+ if prev_unrolled_counter is None:
41
+ prev_unrolled_counter = sample_counter
42
+ else:
43
+ last = prev_unrolled_counter % 256
44
+ if sample_counter < last:
45
+ current_unrolled = prev_unrolled_counter - last + sample_counter + 256
46
+ else:
47
+ current_unrolled = prev_unrolled_counter - last + sample_counter
48
+ 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)
51
+ prev_unrolled_counter = current_unrolled
52
+
53
+ # Set start_time when first sample is received
54
+ if start_time is None:
55
+ 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)
65
+ samples_received += 1
66
+
67
+ 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
+
93
+ print("Found devices:")
94
+ for idx, dev in enumerate(filtered):
95
+ print(f"{idx}: {dev.name} ({dev.address})")
96
+
97
+ # Let the user choose which device to connect to
98
+ 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:
107
+ if not client.is_connected:
108
+ print("Failed to connect")
109
+ return
110
+ print("Connected to", target.name)
111
+ await client.write_gatt_char(CONTROL_CHAR_UUID, b"START", response=True)
112
+ print("Sent START command")
113
+ await client.start_notify(DATA_CHAR_UUID, notification_handler)
114
+ print("Subscribed to data notifications")
115
+ asyncio.create_task(print_rate())
116
+ while True:
117
+ await asyncio.sleep(1)
118
+
119
+ if __name__ == "_main_":
120
+ asyncio.run(run())
0 commit comments