Skip to content

Commit c4c75c8

Browse files
committed
Init BioAmp Tool
1 parent ce49e69 commit c4c75c8

File tree

4 files changed

+305
-1
lines changed

4 files changed

+305
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Recorded data
2+
*.csv
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

README.md

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,85 @@
11
# BioAmp-Tool-Python
2-
A python tool to record data from BioAmp hardware.
2+
3+
A python tool to record data from BioAmp hardware.This project is designed to read data from an Arduino via a serial connection, process the data, and stream it using the Lab Streaming Layer (LSL) protocol. It also logs the processed data to a CSV file for further analysis.
4+
5+
## Features
6+
7+
- Auto-detects connected Arduino devices.
8+
- Reads and processes data packets from Arduino.
9+
- Streams data via LSL for real-time analysis.
10+
- Logs data to a CSV file, including counters and channel data.
11+
- Calculates and logs sampling rate and drift.
12+
- Handles missing samples and prints relevant errors.
13+
14+
## Requirements
15+
16+
- Python 3.7 or higher
17+
- [pySerial](https://pypi.org/project/pyserial/)
18+
- [pylsl](https://pypi.org/project/pylsl/)
19+
- An Arduino device capable of sending serial data packets
20+
21+
## Installation
22+
23+
1. **Clone the repository**:
24+
```bash
25+
git clone https://github.com/upsidedownlabs/BioAmp-Tool-Python.git
26+
```
27+
28+
2. **Install the required Python packages**:
29+
```bash
30+
pip install -r requirements.txt
31+
```
32+
33+
## Usage
34+
35+
1. **Connect your Arduino** to your computer via USB.
36+
37+
2. **Run the script** with the desired options:
38+
```bash
39+
python bioamptool.py --detect
40+
```
41+
42+
3. **View the output** on your console, which will include minute-by-minute data counts, 10-minute summaries, sampling rate , any detected errors or drift.
43+
44+
## Command-line Options
45+
46+
- `-d, --detect`: Auto-detect the Arduino COM port.
47+
- `-p, --port`: Specify the COM port (e.g., `COM3` on Windows or `/dev/ttyUSB0` on Linux).
48+
- `-b, --baudrate`: Set the baud rate for serial communication (default is `57600`).
49+
50+
Example:
51+
```bash
52+
python bioamptool.py --detect
53+
```
54+
55+
## Data Logging
56+
57+
- **CSV Output**: The script saves the processed data in a CSV file named `packet_data.csv`.
58+
- The CSV contains the following columns:
59+
- `Counter`: The sample counter from the Arduino.
60+
- `Channel1` to `Channel6`: The data values from each channel.
61+
62+
- **Log Intervals**: The script logs data counts every minute and provides a summary every 10 minutes, including the sampling rate and drift in seconds per hour.
63+
64+
## LSL Streaming
65+
66+
- **Stream Name**: `BioAmpDataStream`
67+
- **Stream Type**: `EXG`
68+
- **Channel Count**: `6`
69+
- **Sampling Rate**: `250 Hz`
70+
- **Data Format**: `float32`
71+
72+
Use an LSL viewer (e.g., BrainVision Recorder) to visualize the streamed data in real-time.
73+
74+
## Troubleshooting
75+
76+
- **Arduino Not Detected**: Ensure your Arduino is properly connected and recognized by your system. Use the `--detect` option to automatically find the Arduino port.
77+
- **Invalid Data Packets**: If you see messages about invalid data packets, check the data format and synchronization bytes being sent from the Arduino.
78+
- **Zero LSL Stream Utilization**: If the LSL stream shows 0% utilization, verify the stream is properly set up and data is being pushed to the outlet.
79+
80+
## Contributors
81+
82+
We are thankful to our awesome contributors, the list below is alphabetically sorted.
83+
84+
- [Payal Lakra](https://github.com/payallakra)
85+

bioamptool.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# BioAmp Tool
2+
# https://github.com/upsidedownlabs/BioAmp-Tool-Python
3+
#
4+
# Upside Down Labs invests time and resources providing this open source code,
5+
# please support Upside Down Labs and open-source hardware by purchasing
6+
# products from Upside Down Labs!
7+
#
8+
# Copyright (c) 2024 Payal Lakra
9+
# Copyright (c) 2024 Upside Down Labs
10+
#
11+
# Permission is hereby granted, free of charge, to any person obtaining a copy
12+
# of this software and associated documentation files (the "Software"), to deal
13+
# in the Software without restriction, including without limitation the rights
14+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
# copies of the Software, and to permit persons to whom the Software is
16+
# furnished to do so, subject to the following conditions:
17+
#
18+
# The above copyright notice and this permission notice shall be included in all
19+
# copies or substantial portions of the Software.
20+
#
21+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27+
# SOFTWARE.
28+
29+
from pylsl import StreamInfo, StreamOutlet
30+
import argparse
31+
import serial
32+
import time
33+
import csv
34+
from collections import deque
35+
import serial.tools.list_ports
36+
37+
# Initialize global variables
38+
total_packet_count = 0 # Counter for packets received in the current minute
39+
start_time = None # Timestamp for the start of the current time
40+
total_data_received = 0 # Total number of data packets received in the 10-minute interval
41+
previous_sample_number = None # Variable to store the last sample number
42+
missing_samples = 0 # Counter for missing samples
43+
buffer = bytearray()
44+
PACKET_LENGTH = 17
45+
SYNC_BYTE1 = 0xA5
46+
SYNC_BYTE2 = 0x5A
47+
END_BYTE = 0x01
48+
49+
# LSL Stream Setup
50+
lsl_stream_info = StreamInfo('BioAmpDataStream', 'EXG', 6, 250, 'float32', 'UpsideDownLabs') # Define LSL stream info
51+
lsl_outlet = StreamOutlet(lsl_stream_info) # Create LSL outlet for streaming data
52+
53+
def auto_detect_arduino(baudrate, timeout=1):
54+
"""
55+
Auto-detect Arduino by checking all available serial ports.
56+
"""
57+
ports = serial.tools.list_ports.comports() # List all serial ports
58+
for port in ports:
59+
try:
60+
ser = serial.Serial(port.device, baudrate=baudrate, timeout=timeout) # Open serial port
61+
time.sleep(1) # Wait for Arduino to respond
62+
response = ser.readline().strip() # Read response from Arduino
63+
if response:
64+
ser.close() # Close serial port
65+
print(f"Arduino detected at {port.device}") # Print detected port
66+
return port.device # Return the detected port
67+
ser.close() # Close serial port if no response
68+
except (OSError, serial.SerialException):
69+
pass # Handle errors in serial port communication
70+
print("Arduino not detected") # Print message if no Arduino is detected
71+
return None # Return None if no Arduino is detected
72+
73+
def read_arduino_data(ser, csv_writer):
74+
"""
75+
Read data from Arduino, process it, and write to CSV and LSL stream.
76+
"""
77+
global total_packet_count, previous_sample_number, missing_samples, buffer
78+
raw_data = ser.read(ser.in_waiting or 1) # Read 17 bytes from the serial port
79+
buffer.extend(raw_data)
80+
81+
# Check for valid data packet structure
82+
while len(buffer) >= PACKET_LENGTH:
83+
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2]))
84+
85+
if sync_index == -1:
86+
buffer.clear
87+
continue
88+
89+
if(len(buffer) >= sync_index + PACKET_LENGTH):
90+
packet = buffer[sync_index:sync_index+PACKET_LENGTH]
91+
print(packet)
92+
if len(packet) == 17 and packet[0] == SYNC_BYTE1 and packet[1] == SYNC_BYTE2 and packet[-1] == END_BYTE:
93+
counter = packet[3] # Counter is at index 3
94+
95+
# Ensure counter number is exactly one more than the previous one
96+
if previous_sample_number is not None and counter != (previous_sample_number + 1) % 256:
97+
missing_samples += (counter - previous_sample_number - 1) % 256
98+
print(f"Error: Expected counter {previous_sample_number + 1} but received {counter}. Missing samples: {missing_samples}")
99+
exit()
100+
101+
previous_sample_number = counter # Update previous sample number to current counter
102+
103+
total_packet_count += 1 # Increment packet count only after initial samples are ignored
104+
105+
# Merge high and low bytes to form channel data
106+
channel_data = []
107+
for i in range(4, 16, 2): # Indices for channel data
108+
high_byte = packet[i]
109+
low_byte = packet[i + 1]
110+
value = (high_byte << 8) | low_byte # Combine high and low bytes to form the 16-bit value
111+
channel_data.append(float(value))
112+
113+
# Write counter and channel data to CSV
114+
csv_writer.writerow([counter] + channel_data)
115+
116+
# Push channel data to LSL stream
117+
lsl_outlet.push_sample(channel_data)
118+
119+
del buffer[:sync_index + PACKET_LENGTH]
120+
else:
121+
del buffer[:sync_index + 1]
122+
print("Invalid Data Packet") # Print message if data packet is invalid
123+
124+
def start_timer():
125+
"""
126+
Initialize timers for minute and ten-minute intervals and reset packet count.
127+
"""
128+
global start_time, total_packet_count
129+
current_time = time.time() # Get current timestamp
130+
start_time = current_time # Set start time
131+
total_packet_count = 0 # Reset packet count
132+
133+
def log_minute_data():
134+
"""
135+
Logs and resets data count per minute.
136+
"""
137+
global total_packet_count
138+
count_for_minute = total_packet_count # Get the count for the current minute
139+
print(f"Data count for this minute: {count_for_minute} samples") # Print count for the current minute
140+
total_packet_count = 0 # Reset packet count for the next minute
141+
return count_for_minute # Return the count for further processing
142+
143+
def log_ten_minute_data():
144+
"""
145+
Logs data count for every 10 minutes and computes sampling rate and drift.
146+
"""
147+
global total_data_received, start_time
148+
149+
# Calculate total data count and sampling rate
150+
print(f"Total data count after 10 minutes: {total_data_received} samples") # Print total data count for the last 10 minutes
151+
sampling_rate = total_data_received / (10 * 60) # Calculate sampling rate
152+
print(f"Sampling rate: {sampling_rate:.2f} samples/second") # Print sampling rate
153+
154+
# Calculate drift
155+
expected_sampling_rate = 250 # Expected sampling rate
156+
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift in seconds/hour
157+
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
158+
159+
# Reset for the next 10-minute interval
160+
total_data_received = 0 # Reset total data received
161+
start_time = time.time() # Update start time for the next 10-minute interval
162+
163+
def parse_data(port, baudrate):
164+
"""
165+
Main function to process data from the Arduino.
166+
"""
167+
global total_packet_count, start_time, start_time, total_data_received
168+
169+
with serial.Serial(port, baudrate, timeout=0.1) as ser:
170+
with open('packet_data.csv', mode='w', newline='') as csv_file:
171+
csv_writer = csv.writer(csv_file)
172+
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write the CSV header
173+
174+
try:
175+
time.sleep(2) # Allow time for Arduino to initialize
176+
177+
start_timer() # Initialize timer
178+
179+
while True:
180+
read_arduino_data(ser, csv_writer) # Read data from Arduino
181+
182+
# if initial_samples_ignored:
183+
current_time = time.time() # Get current timestamp
184+
185+
# Handle minute interval
186+
if current_time - start_time >= 60:
187+
total_data_received += log_minute_data() # Log minute data and add to total
188+
start_time = current_time # Reset minute timer
189+
190+
# Handle 10-minute interval
191+
if current_time - start_time >= 600:
192+
total_data_received += log_minute_data() # Log last minute before the 10-minute interval ends
193+
log_ten_minute_data() # Log data for the 10-minute interval
194+
start_timer() # Reset timers to prevent a partial minute log after the 10-minute interval
195+
196+
except KeyboardInterrupt:
197+
# Handle keyboard interrupt
198+
print(f"Exiting. \nTotal missing samples: {missing_samples}") # Print missing samples and exit
199+
200+
if __name__ == "__main__":
201+
parser = argparse.ArgumentParser(description="Upside Down Labs - BioAmp Tool")
202+
parser.add_argument('-d', '--detect', action='store_true', help="Auto-detect Arduino") # Argument to auto-detect Arduino
203+
parser.add_argument('-p', '--port', type=str, help="Specify the COM port") # Argument to specify COM port
204+
parser.add_argument('-b', '--baudrate', type=int, default=57600, help="Set baud rate for the serial communication") # Argument for baud rate
205+
206+
args = parser.parse_args() # Parse command-line arguments
207+
208+
if args.detect:
209+
port = auto_detect_arduino(baudrate=args.baudrate) # Auto-detect Arduino if specified
210+
else:
211+
port = args.port # Use specified port
212+
213+
if port is None:
214+
print("Arduino port not specified or detected. Exiting.")
215+
else:
216+
parse_data(port, args.baudrate) # Start processing data

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pyserial==3.5
2+
pylsl==1.14.0

0 commit comments

Comments
 (0)