Skip to content

Commit 3bf25b7

Browse files
committed
NPG-LITE BLE Code
1 parent 8cdb4f1 commit 3bf25b7

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed

npg-ble

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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

Comments
 (0)