Skip to content

Commit a5f5a36

Browse files
authored
Merge pull request #3 from PayalLakra/bio_amptool
Add verbose, csv, and lsl options
2 parents 9d712fa + 16bd874 commit a5f5a36

File tree

1 file changed

+160
-143
lines changed

1 file changed

+160
-143
lines changed

bioamptool.py

Lines changed: 160 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -26,188 +26,205 @@
2626
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2727
# SOFTWARE.
2828

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
29+
30+
from pylsl import StreamInfo, StreamOutlet # Import LSL stream classes for data streaming
31+
import argparse # Import argparse for command-line argument parsing
32+
import serial # Import serial for serial communication with Arduino
33+
import time # Import time for handling time-related functions
34+
import csv # Import csv for handling CSV file operations
35+
from datetime import datetime # Import datetime for date and time operations
36+
import serial.tools.list_ports # Import tools for listing available serial ports
3637

3738
# 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
39+
total_packet_count = 0 # Total number of packets received
40+
start_time = None # Start time of data collection
41+
last_ten_minute_time = None # Time when the last 10-minute interval started
42+
total_data_received = 0 # Accumulated total number of data points received
43+
previous_sample_number = None # Last received sample number to detect missing samples
44+
missing_samples = 0 # Count of missing samples
45+
buffer = bytearray() # Buffer to accumulate incoming data bytes
46+
PACKET_LENGTH = 17 # Length of each data packet
47+
SYNC_BYTE1 = 0xA5 # First sync byte value
48+
SYNC_BYTE2 = 0x5A # Second sync byte value
49+
END_BYTE = 0x01 # End byte value for the packet
50+
verbose_mode = False # Flag for verbose output
51+
lsl_outlet = None # LSL outlet for streaming data
52+
data_count_10_sec = 0 # Count of data points in the last 10 seconds
53+
last_10_sec_time = None # Last time when the 10-second count was logged
54+
55+
def auto_detect_arduino(baudrate, timeout=1): # Auto-detect Arduino by checking all available serial ports.
56+
ports = serial.tools.list_ports.comports() # List all available serial ports
5857
for port in ports:
5958
try:
6059
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
60+
time.sleep(1) # Wait for Arduino to initialize
61+
response = ser.readline().strip() # Read data from Arduino
6362
if response:
64-
ser.close() # Close serial port
65-
print(f"Arduino detected at {port.device}") # Print detected port
63+
ser.close() # Close the serial port if response is found
64+
print(f"Arduino detected at {port.device}") # Print detected port
6665
return port.device # Return the detected port
67-
ser.close() # Close serial port if no response
66+
ser.close() # Close the serial port if no response
6867
except (OSError, serial.SerialException):
69-
pass # Handle errors in serial port communication
70-
print("Arduino not detected") # Print message if no Arduino is detected
68+
pass # Handle any exceptions and continue scanning other ports
69+
print("Arduino not detected") # Print message if no Arduino is found
7170
return None # Return None if no Arduino is detected
7271

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)
72+
def read_arduino_data(ser, csv_writer=None): # Read data from Arduino, process it, and optionally write to CSV and LSL stream.
73+
global total_packet_count, previous_sample_number, missing_samples, buffer, data_count_10_sec
74+
raw_data = ser.read(ser.in_waiting or 1) # Read data from the serial port
75+
buffer.extend(raw_data) # Append the read data to the buffer
8076

81-
# Check for valid data packet structure
82-
while len(buffer) >= PACKET_LENGTH:
83-
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2]))
77+
while len(buffer) >= PACKET_LENGTH: # Process data if buffer has enough bytes
78+
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2])) # Find sync bytes in buffer
8479

85-
if sync_index == -1:
86-
buffer.clear
80+
if sync_index == -1: # If sync bytes are not found
81+
buffer.clear() # Clear the buffer and continue
8782
continue
8883

89-
if(len(buffer) >= sync_index + PACKET_LENGTH):
90-
packet = buffer[sync_index:sync_index+PACKET_LENGTH]
84+
if len(buffer) >= sync_index + PACKET_LENGTH: # Check if buffer has a complete packet
85+
packet = buffer[sync_index:sync_index + PACKET_LENGTH] # Extract the packet from the buffer
9186
if len(packet) == 17 and packet[0] == SYNC_BYTE1 and packet[1] == SYNC_BYTE2 and packet[-1] == END_BYTE:
92-
counter = packet[3] # Counter is at index 3
87+
counter = packet[3] # Extract the counter value from the packet
9388

94-
# Ensure counter number is exactly one more than the previous one
9589
if previous_sample_number is not None and counter != (previous_sample_number + 1) % 256:
90+
# Check for missing samples based on the counter
9691
missing_samples += (counter - previous_sample_number - 1) % 256
9792
print(f"Error: Expected counter {previous_sample_number + 1} but received {counter}. Missing samples: {missing_samples}")
9893

99-
previous_sample_number = counter # Update previous sample number to current counter
100-
101-
total_packet_count += 1 # Increment packet count only after initial samples are ignored
102-
103-
# Merge high and low bytes to form channel data
104-
channel_data = []
105-
for i in range(4, 16, 2): # Indices for channel data
106-
high_byte = packet[i]
107-
low_byte = packet[i + 1]
108-
value = (high_byte << 8) | low_byte # Combine high and low bytes to form the 16-bit value
109-
channel_data.append(float(value))
94+
previous_sample_number = counter # Update the previous sample number
95+
total_packet_count += 1 # Increment the total packet count
96+
data_count_10_sec += 1 # Increment 10-second data count
11097

111-
# Write counter and channel data to CSV
112-
csv_writer.writerow([counter] + channel_data)
98+
channel_data = [] # List to store channel data
99+
for i in range(4, 16, 2): # Extract channel data from the packet
100+
high_byte = packet[i] # High byte of the data
101+
low_byte = packet[i + 1] # Low byte of the data
102+
value = (high_byte << 8) | low_byte # Combine high and low byte into a value
103+
channel_data.append(float(value)) # Append the value to the channel data list
113104

114-
# Push channel data to LSL stream
115-
lsl_outlet.push_sample(channel_data)
105+
if csv_writer:
106+
csv_writer.writerow([counter] + channel_data) # Write data to CSV file
107+
if lsl_outlet:
108+
lsl_outlet.push_sample(channel_data) # Send data to LSL stream
116109

117-
del buffer[:sync_index + PACKET_LENGTH]
110+
del buffer[:sync_index + PACKET_LENGTH] # Remove processed packet from the buffer
118111
else:
119-
del buffer[:sync_index + 1]
120-
121-
def start_timer():
122-
"""
123-
Initialize timers for minute and ten-minute intervals and reset packet count.
124-
"""
125-
global start_time, total_packet_count
126-
current_time = time.time() # Get current timestamp
127-
start_time = current_time # Set start time
128-
total_packet_count = 0 # Reset packet count
129-
130-
def log_minute_data():
131-
"""
132-
Logs and resets data count per minute.
133-
"""
112+
del buffer[:sync_index + 1] # Remove invalid data from the buffer
113+
114+
def start_timer(): # Initialize timers for minute and ten-minute intervals and reset packet count.
115+
global start_time, last_ten_minute_time, total_packet_count, last_10_sec_time, data_count_10_sec
116+
time.sleep(0.5) # Ensure LSL stream setup is complete
117+
current_time = time.time() # Get the current time
118+
start_time = current_time # Set the start time
119+
last_ten_minute_time = current_time # Set the time for the last ten-minute interval
120+
last_10_sec_time = current_time # Set the start time for the 10-second interval
121+
total_packet_count = 0 # Reset total packet count
122+
data_count_10_sec = 0 # Reset 10-second data count
123+
124+
def log_minute_data(): # Logs and resets data count per minute
134125
global total_packet_count
135-
count_for_minute = total_packet_count # Get the count for the current minute
136-
print(f"Data count for this minute: {count_for_minute} samples") # Print count for the current minute
137-
total_packet_count = 0 # Reset packet count for the next minute
138-
return count_for_minute # Return the count for further processing
139-
140-
def log_ten_minute_data():
141-
"""
142-
Logs data count for every 10 minutes and computes sampling rate and drift.
143-
"""
144-
global total_data_received, start_time
145-
146-
# Calculate total data count and sampling rate
147-
print(f"Total data count after 10 minutes: {total_data_received} samples") # Print total data count for the last 10 minutes
148-
sampling_rate = total_data_received / (10 * 60) # Calculate sampling rate
149-
print(f"Sampling rate: {sampling_rate:.2f} samples/second") # Print sampling rate
150-
151-
# Calculate drift
152-
expected_sampling_rate = 250 # Expected sampling rate
153-
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift in seconds/hour
154-
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
126+
count_for_minute = total_packet_count # Get the data count for the current minute
127+
print(f"Data count for this minute: {count_for_minute} samples") # Print the data count
128+
total_packet_count = 0 # Reset total packet count for the next minute
129+
return count_for_minute # Return the count for further use
130+
131+
def log_ten_minute_data(): # Logs data count for every 10 minutes and computes sampling rate and drift.
132+
global total_data_received, last_ten_minute_time
155133

156-
# Reset for the next 10-minute interval
157-
total_data_received = 0 # Reset total data received
158-
start_time = time.time() # Update start time for the next 10-minute interval
134+
print(f"Total data count after 10 minutes: {total_data_received} samples") # Print total data count
135+
sampling_rate = total_data_received / (10 * 60) # Calculate the sampling rate
136+
print(f"Sampling rate: {sampling_rate:.2f} samples/second") # Print the sampling rate
159137

160-
def parse_data(port, baudrate):
161-
"""
162-
Main function to process data from the Arduino.
163-
"""
164-
global total_packet_count, start_time, start_time, total_data_received
138+
expected_sampling_rate = 250 # Expected sampling rate
139+
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift in seconds per hour
140+
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
165141

166-
with serial.Serial(port, baudrate, timeout=0.1) as ser:
167-
with open('packet_data.csv', mode='w', newline='') as csv_file:
168-
csv_writer = csv.writer(csv_file)
169-
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write the CSV header
142+
total_data_received = 0 # Reset total data received for the next 10-minute interval
143+
last_ten_minute_time = time.time() # Update the last ten-minute interval start
170144

171-
try:
172-
time.sleep(2) # Allow time for Arduino to initialize
145+
def parse_data(port, baudrate, lsl_flag=False, csv_flag=False, verbose_flag=False): # Main function to process data from the Arduino.
146+
global total_packet_count, start_time, total_data_received, lsl_outlet, last_ten_minute_time
147+
global data_count_10_sec, last_10_sec_time, verbose_mode
173148

174-
start_timer() # Initialize timer
149+
verbose_mode = verbose_flag # Set the verbose mode flag
175150

176-
while True:
177-
read_arduino_data(ser, csv_writer) # Read data from Arduino
151+
csv_writer = None # CSV writer is initially None
152+
csv_filename = None # CSV filename is initially None
178153

179-
# if initial_samples_ignored:
180-
current_time = time.time() # Get current timestamp
154+
# Check if LSL stream is enabled
155+
if lsl_flag:
156+
lsl_stream_info = StreamInfo('BioAmpDataStream', 'EXG', 6, 250, 'float32', 'UpsideDownLabs') # Define LSL stream info
157+
lsl_outlet = StreamOutlet(lsl_stream_info) # Create LSL outlet
158+
print("LSL stream started") # Print message indicating LSL stream has started
159+
160+
# If CSV logging is requested
161+
if csv_flag:
162+
csv_filename = f"data_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv" # Generate the filename dynamically based on current date and time
163+
print(f"CSV recording started. Data will be saved to {csv_filename}") # Print CSV recording message
181164

182-
# Handle minute interval
183-
if current_time - start_time >= 60:
184-
total_data_received += log_minute_data() # Log minute data and add to total
185-
start_time = current_time # Reset minute timer
165+
# Open the serial port and CSV file
166+
with serial.Serial(port, baudrate, timeout=0.1) as ser: # Open serial port
167+
csv_file = open(csv_filename, mode='w', newline='') if csv_flag else None # Open CSV file if specified
168+
169+
if csv_file:
170+
csv_writer = csv.writer(csv_file) # Create CSV writer
171+
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write CSV header
186172

187-
# Handle 10-minute interval
188-
if current_time - start_time >= 600:
189-
total_data_received += log_minute_data() # Log last minute before the 10-minute interval ends
190-
log_ten_minute_data() # Log data for the 10-minute interval
191-
start_timer() # Reset timers to prevent a partial minute log after the 10-minute interval
173+
# Delay to account for initial data and ensure accurate timing
174+
start_timer() # Initialize timers and reset counters
192175

193-
except KeyboardInterrupt:
194-
# Handle keyboard interrupt
195-
print(f"Exiting. \nTotal missing samples: {missing_samples}") # Print missing samples and exit
176+
try:
177+
while True:
178+
current_time = time.time() # Get the current time
179+
180+
# Read and process data from Arduino
181+
read_arduino_data(ser, csv_writer)
182+
183+
# Check if 10 seconds have passed and log data count
184+
if current_time - last_10_sec_time >= 10:
185+
if verbose_mode:
186+
print(f"Verbose: Data count in the last 10 seconds: {data_count_10_sec}") # Print verbose data count
187+
data_count_10_sec = 0 # Reset 10-second data count
188+
last_10_sec_time = current_time # Update 10-second interval start
189+
190+
# Check if a minute has passed and log minute data
191+
if current_time - start_time >= 60:
192+
total_data_received += log_minute_data() # Update total data received
193+
start_time += 60 # Adjust the start time to handle the next interval accurately
194+
195+
# Check if 10 minutes have passed and log ten-minute data
196+
if current_time - last_ten_minute_time >= 600:
197+
total_data_received += log_minute_data() # Update total data received
198+
log_ten_minute_data() # Log ten-minute data
199+
start_timer() # Restart timers
200+
201+
except KeyboardInterrupt:
202+
if csv_file:
203+
csv_file.close() # Close CSV file
204+
print(f"CSV recording stopped. Data saved to {csv_filename}.") # Print message indicating CSV recording stopped
205+
print(f"Exiting. \nTotal missing samples: {missing_samples}") # Print total missing samples
196206

197207
if __name__ == "__main__":
208+
# Argument parser for command-line interface
198209
parser = argparse.ArgumentParser(description="Upside Down Labs - BioAmp Tool")
199-
parser.add_argument('-d', '--detect', action='store_true', help="Auto-detect Arduino") # Argument to auto-detect Arduino
200-
parser.add_argument('-p', '--port', type=str, help="Specify the COM port") # Argument to specify COM port
201-
parser.add_argument('-b', '--baudrate', type=int, default=57600, help="Set baud rate for the serial communication") # Argument for baud rate
210+
parser.add_argument('-p', '--port', type=str, help="Specify the COM port")
211+
parser.add_argument('-b', '--baudrate', type=int, default=57600, help="Set baud rate for the serial communication")
212+
parser.add_argument('--csv', action='store_true', help="Create and write to a CSV file")
213+
parser.add_argument('--lsl', action='store_true', help="Start LSL stream")
214+
parser.add_argument('-v', '--verbose', action='store_true', help="Verbose mode for data count every 10 seconds")
202215

203216
args = parser.parse_args() # Parse command-line arguments
204217

205-
if args.detect:
206-
port = auto_detect_arduino(baudrate=args.baudrate) # Auto-detect Arduino if specified
207-
else:
208-
port = args.port # Use specified port
209-
210-
if port is None:
211-
print("Arduino port not specified or detected. Exiting.")
218+
# Determine port and start data parsing based on arguments
219+
if args.lsl:
220+
if args.port:
221+
port = args.port # Use specified port
222+
else:
223+
port = auto_detect_arduino(baudrate=args.baudrate) # Auto-detect Arduino port
224+
if port is None:
225+
print("Arduino port not specified or detected. Exiting.") # Print message if no port detected
226+
else:
227+
parse_data(port, args.baudrate, lsl_flag=args.lsl, csv_flag=args.csv, verbose_flag=args.verbose) # Start data parsing
212228
else:
213-
parse_data(port, args.baudrate) # Start processing data
229+
parser.print_help() # Print help message if no valid arguments are provided
230+

0 commit comments

Comments
 (0)