Skip to content

Commit 490cbc8

Browse files
authored
Merge pull request #10 from PayalLakra/bio_amptool
Update code for new chords format & add functionality to stop program on 'q' key press.
2 parents 1d27980 + dec6b30 commit 490cbc8

File tree

3 files changed

+50
-23
lines changed

3 files changed

+50
-23
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
## BioAmp Tool - Python
1+
# BioAmp Tool - Python
22

33
The BioAmp Tool is a Python script designed to interface with an Arduino-based bioamplifier, read data from it, optionally log this data to CSV or stream it via the Lab Streaming Layer (LSL), and visualize it through a graphical user interface (GUI) with live plotting.
44

5+
**_Note_**: Flash Arduino code to your hardware from [Chords Arduino Firmware](https://github.com/upsidedownlabs/Chords-Arduino-Firmware) to use this python tool.
6+
57
## Features
68

79
- **Automatic Arduino Detection:** Automatically detects connected Arduino devices via serial ports.
@@ -13,7 +15,7 @@ The BioAmp Tool is a Python script designed to interface with an Arduino-based b
1315

1416
## Requirements
1517

16-
- Python 3.x
18+
- Python 3.x
1719
- `pyserial` library (for serial communication)
1820
- `pylsl` library (for LSL streaming)
1921
- `argparse`, `time`, `csv`, `datetime` (standard libraries)
@@ -102,7 +104,8 @@ Handles command-line argument parsing and initiates data processing.
102104
- **Stream Name**: `BioAmpDataStream`
103105
- **Stream Type**: `EXG`
104106
- **Channel Count**: `6`
105-
- **Sampling Rate**: `250 Hz`
107+
- **Sampling Rate**: `250 Hz` for `UNO-R3`
108+
: `500 Hz` for `UNO-R4`
106109
- **Data Format**: `float32`
107110
108111
If GUI is not enabled, you can use an LSL viewer (e.g., BrainVision LSL Viewer) to visualize the streamed data in real-time.

bioamptool.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import numpy as np # For handling numeric arrays
3939
import pyqtgraph as pg # For real-time plotting
4040
from pyqtgraph.Qt import QtWidgets, QtCore # PyQt components for GUI
41+
import msvcrt #For keyboard interruptions
4142

4243
# Initialize global variables for tracking and processing data
4344
total_packet_count = 0 # Total packets received in the last second
@@ -47,27 +48,40 @@
4748
previous_sample_number = None # Store the previous sample number for detecting missing samples
4849
missing_samples = 0 # Count of missing samples due to packet loss
4950
buffer = bytearray() # Buffer for storing incoming raw data from Arduino
50-
PACKET_LENGTH = 17 # Expected length of each data packet
51-
SYNC_BYTE1 = 0xA5 # First byte of sync marker
52-
SYNC_BYTE2 = 0x5A # Second byte of sync marker
51+
NUM_CHANNELS = 6 #Number of Channels being received
52+
data = np.zeros((6, 2000)) # 2D array to store data for real-time plotting (6 channels, 2000 data points)
53+
samples_per_second = 0 # Number of samples received per second
54+
55+
# Initialize gloabal variables for Arduino Board
56+
board = "" # Variable for Connected Arduino Board
57+
boards_sample_rate = {"UNO-R3":250, "UNO-R4":500} #Standard Sample rate for Arduino Boards Different Firmware
58+
59+
# Initialize gloabal variables for Incoming Data
60+
PACKET_LENGTH = 16 # Expected length of each data packet
61+
SYNC_BYTE1 = 0xc7 # First byte of sync marker
62+
SYNC_BYTE2 = 0x7c # Second byte of sync marker
5363
END_BYTE = 0x01 # End byte marker
64+
HEADER_LENGTH = 3 #Length of the Packet Header
65+
66+
## Initialize gloabal variables for Output
5467
lsl_outlet = None # Placeholder for LSL stream outlet
5568
verbose = False # Flag for verbose output mode
56-
data = np.zeros((6, 2000)) # 2D array to store data for real-time plotting (6 channels, 2000 data points)
5769
csv_filename = None # Store CSV filename
58-
samples_per_second = 0 # Number of samples received per second
5970

6071
# Function to automatically detect the Arduino's serial port
6172
def auto_detect_arduino(baudrate, timeout=1):
6273
ports = serial.tools.list_ports.comports() # List available serial ports
6374
for port in ports: # Iterate through each port
6475
try:
6576
ser = serial.Serial(port.device, baudrate=baudrate, timeout=timeout) # Try opening the port
77+
ser.write(b'WHORU\n')
6678
time.sleep(1) # Wait for the device to initialize
67-
response = ser.readline().strip() # Try reading from the port
79+
response = ser.readline().strip().decode() # Try reading from the port
6880
if response: # If response is received, assume it's the Arduino
81+
global board
6982
ser.close() # Close the serial connection
70-
print(f"Arduino detected at {port.device}") # Notify the user
83+
print(f"{response} detected at {port.device}") # Notify the user
84+
board = response
7185
return port.device # Return the port name
7286
ser.close() # Close the port if no response
7387
except (OSError, serial.SerialException): # Handle exceptions if the port can't be opened
@@ -81,7 +95,6 @@ def read_arduino_data(ser, csv_writer=None):
8195

8296
raw_data = ser.read(ser.in_waiting or 1) # Read available data from the serial port
8397
buffer.extend(raw_data) # Add received data to the buffer
84-
8598
while len(buffer) >= PACKET_LENGTH: # Continue processing if the buffer contains at least one full packet
8699
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2])) # Search for the sync marker
87100

@@ -93,7 +106,7 @@ def read_arduino_data(ser, csv_writer=None):
93106
packet = buffer[sync_index:sync_index + PACKET_LENGTH] # Extract the packet
94107
if len(packet) == PACKET_LENGTH and packet[0] == SYNC_BYTE1 and packet[1] == SYNC_BYTE2 and packet[-1] == END_BYTE:
95108
# Extract the packet if it is valid (correct length, sync bytes, and end byte)
96-
counter = packet[3] # Read the counter byte (for tracking sample order)
109+
counter = packet[2] # Read the counter byte (for tracking sample order)
97110

98111
# Check for missing samples by comparing the counter values
99112
if previous_sample_number is not None and counter != (previous_sample_number + 1) % 256:
@@ -107,9 +120,9 @@ def read_arduino_data(ser, csv_writer=None):
107120

108121
# Extract channel data (6 channels, 2 bytes per channel)
109122
channel_data = []
110-
for i in range(4, 16, 2): # Loop through channel data bytes
111-
high_byte = packet[i]
112-
low_byte = packet[i + 1]
123+
for channel in range(NUM_CHANNELS): # Loop through channel data bytes
124+
high_byte = packet[2*channel + HEADER_LENGTH]
125+
low_byte = packet[2*channel + HEADER_LENGTH + 1]
113126
value = (high_byte << 8) | low_byte # Combine high and low bytes
114127
channel_data.append(float(value)) # Convert to float and add to channel data
115128

@@ -208,7 +221,7 @@ def log_ten_minute_data(verbose=False):
208221
print(f"Total data count after 10 minutes: {cumulative_packet_count}") # Print cumulative data count
209222
sampling_rate = cumulative_packet_count / (10 * 60) # Calculate sampling rate
210223
print(f"Sampling rate: {sampling_rate:.2f} samples/second") # Print sampling rate
211-
expected_sampling_rate = 250 # Expected sampling rate
224+
expected_sampling_rate = boards_sample_rate[board] # Expected sampling rate
212225
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift
213226
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
214227
cumulative_packet_count = 0 # Reset cumulative packet count
@@ -222,7 +235,7 @@ def parse_data(port, baudrate, lsl_flag=False, csv_flag=False, gui_flag=False, v
222235

223236
# Start LSL streaming if requested
224237
if lsl_flag:
225-
lsl_stream_info = StreamInfo('BioAmpDataStream', 'EXG', 6, 250, 'float32', 'UpsideDownLabs') # Define LSL stream info
238+
lsl_stream_info = StreamInfo('BioAmpDataStream', 'EXG', 6, boards_sample_rate[board], 'float32', 'UpsideDownLabs') # Define LSL stream info
226239
lsl_outlet = StreamOutlet(lsl_stream_info) # Create LSL outlet
227240
print("LSL stream started") # Notify user
228241
time.sleep(0.5) # Wait for the LSL stream to start
@@ -249,6 +262,7 @@ def parse_data(port, baudrate, lsl_flag=False, csv_flag=False, gui_flag=False, v
249262
start_timer() # Start timers for logging
250263

251264
try:
265+
ser.write(b'START\n')
252266
while True:
253267
read_arduino_data(ser, csv_writer) # Read and process data from Arduino
254268
current_time = time.time() # Get the current time
@@ -262,13 +276,22 @@ def parse_data(port, baudrate, lsl_flag=False, csv_flag=False, gui_flag=False, v
262276
log_ten_minute_data(verbose) # Log data for the last 10 minutes
263277
if gui_flag:
264278
QtWidgets.QApplication.processEvents() # Process GUI events if GUI is enabled
265-
266-
except KeyboardInterrupt: # Handle interruption (Ctrl+C)
279+
280+
if msvcrt.kbhit() and msvcrt.getch() == b'q': # Exit the loop if 'q' is pressed
281+
ser.write(b'STOP\n')
282+
print("Process interrupted by user")
283+
break
284+
285+
except KeyboardInterrupt:
286+
ser.write(b'STOP\n')
287+
print("Process interrupted by user")
288+
289+
finally:
267290
if csv_file:
268-
csv_file.close() # Close CSV file
269-
print(f"CSV recording stopped. Data saved to {csv_filename}.") # Notify user
291+
csv_file.close()
292+
print(f"CSV recording saved as {csv_filename}")
270293
print(f"Exiting.\nTotal missing samples: {missing_samples}") # Print final missing samples count
271-
294+
272295
# Main entry point of the script
273296
def main():
274297
global verbose

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
numpy==2.1.1
22
pylsl==1.16.2
33
pyqtgraph==0.13.7
4-
pyserial==3.5
4+
pyserial==3.5
5+
PySide2==5.15.2.1

0 commit comments

Comments
 (0)