31
31
import serial
32
32
import time
33
33
import csv
34
- from collections import deque
34
+ from datetime import datetime
35
35
import serial .tools .list_ports
36
36
37
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
38
+ total_packet_count = 0 # Total number of packets received
39
+ start_time = None # Start time of data collection
40
+ last_ten_minute_time = None # Time when the last 10-minute interval started
41
+ total_data_received = 0 # Accumulated total number of data points received
42
+ previous_sample_number = None # Last received sample number to detect missing samples
43
+ missing_samples = 0 # Count of missing samples
44
+ buffer = bytearray () # Buffer to accumulate incoming data bytes
45
+ PACKET_LENGTH = 17 # Length of each data packet
46
+ SYNC_BYTE1 = 0xA5 # First sync byte value
47
+ SYNC_BYTE2 = 0x5A # Second sync byte value
48
+ END_BYTE = 0x01 # End byte value for the packet
48
49
49
50
# 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
51
+ lsl_outlet = None # LSL outlet for streaming data
52
+
53
+ def auto_detect_arduino (baudrate , timeout = 1 ): # Auto-detect Arduino by checking all available serial ports.
54
+ ports = serial .tools .list_ports .comports () # List all available serial ports
58
55
for port in ports :
59
56
try :
60
57
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
58
+ time .sleep (1 ) # Wait for Arduino to initialize
59
+ response = ser .readline ().strip () # Read data from Arduino
63
60
if response :
64
- ser .close () # Close serial port
65
- print (f"Arduino detected at { port .device } " ) # Print detected port
61
+ ser .close () # Close the serial port if response is found
62
+ print (f"Arduino detected at { port .device } " ) # Print detected port
66
63
return port .device # Return the detected port
67
- ser .close () # Close serial port if no response
64
+ ser .close () # Close the serial port if no response
68
65
except (OSError , serial .SerialException ):
69
- pass # Handle errors in serial port communication
70
- print ("Arduino not detected" ) # Print message if no Arduino is detected
66
+ pass # Handle any exceptions and continue scanning other ports
67
+ print ("Arduino not detected" ) # Print message if no Arduino is found
71
68
return None # Return None if no Arduino is detected
72
69
73
- def read_arduino_data (ser , csv_writer ):
74
- """
75
- Read data from Arduino, process it, and write to CSV and LSL stream.
76
- """
70
+ def read_arduino_data (ser , csv_writer = None ): #Read data from Arduino, process it, and optionally write to CSV and LSL stream.
77
71
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
+ raw_data = ser .read (ser .in_waiting or 1 ) # Read data from the serial port
73
+ buffer .extend (raw_data ) # Append the read data to the buffer
80
74
81
- # Check for valid data packet structure
82
- while len (buffer ) >= PACKET_LENGTH :
83
- sync_index = buffer .find (bytes ([SYNC_BYTE1 , SYNC_BYTE2 ]))
75
+ while len (buffer ) >= PACKET_LENGTH : # Process data if buffer has enough bytes
76
+ sync_index = buffer .find (bytes ([SYNC_BYTE1 , SYNC_BYTE2 ])) # Find sync bytes in buffer
84
77
85
- if sync_index == - 1 :
86
- buffer .clear
78
+ if sync_index == - 1 : # If sync bytes are not found
79
+ buffer .clear () # Clear the buffer and continue
87
80
continue
88
81
89
- if ( len (buffer ) >= sync_index + PACKET_LENGTH ):
90
- packet = buffer [sync_index :sync_index + PACKET_LENGTH ]
82
+ if len (buffer ) >= sync_index + PACKET_LENGTH : # Check if buffer has a complete packet
83
+ packet = buffer [sync_index :sync_index + PACKET_LENGTH ] # Extract the packet from the buffer
91
84
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
85
+ counter = packet [3 ] # Extract the counter value from the packet
93
86
94
- # Ensure counter number is exactly one more than the previous one
95
87
if previous_sample_number is not None and counter != (previous_sample_number + 1 ) % 256 :
88
+ # Check for missing samples based on the counter
96
89
missing_samples += (counter - previous_sample_number - 1 ) % 256
97
90
print (f"Error: Expected counter { previous_sample_number + 1 } but received { counter } . Missing samples: { missing_samples } " )
98
91
99
- previous_sample_number = counter # Update previous sample number to current counter
92
+ previous_sample_number = counter # Update the previous sample number
93
+ total_packet_count += 1 # Increment the total packet count
100
94
101
- total_packet_count += 1 # Increment packet count only after initial samples are ignored
95
+ channel_data = [] # List to store channel data
96
+ for i in range (4 , 16 , 2 ): # Extract channel data from the packet
97
+ high_byte = packet [i ] # High byte of the data
98
+ low_byte = packet [i + 1 ] # Low byte of the data
99
+ value = (high_byte << 8 ) | low_byte # Combine high and low byte into a value
100
+ channel_data .append (float (value )) # Append the value to the channel data list
102
101
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 ))
102
+ if csv_writer :
103
+ csv_writer .writerow ([counter ] + channel_data ) # Write data to CSV file
104
+ if lsl_outlet :
105
+ lsl_outlet .push_sample (channel_data ) # Send data to LSL stream
110
106
111
- # Write counter and channel data to CSV
112
- csv_writer .writerow ([counter ] + channel_data )
107
+ del buffer [:sync_index + PACKET_LENGTH ] # Remove processed packet from the buffer
108
+ else :
109
+ del buffer [:sync_index + 1 ] # Remove invalid data from the buffer
113
110
114
- # Push channel data to LSL stream
115
- lsl_outlet .push_sample (channel_data )
111
+ def start_timer (): # Initialize timers for minute and ten-minute intervals and reset packet count.
112
+ global start_time , last_ten_minute_time , total_packet_count
113
+ time .sleep (0.5 ) # Ensure LSL stream setup is complete
114
+ current_time = time .time () # Get the current time
115
+ start_time = current_time # Set the start time
116
+ last_ten_minute_time = current_time # Set the time for the last ten-minute interval
117
+ total_packet_count = 0 # Reset total packet count
116
118
117
- del buffer [:sync_index + PACKET_LENGTH ]
118
- 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
- """
119
+ def log_minute_data (): #Logs and resets data count per minute
134
120
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
121
+ count_for_minute = total_packet_count # Get the data count for the current minute
122
+ print (f"Data count for this minute: { count_for_minute } samples" ) # Print the data count
123
+ total_packet_count = 0 # Reset total packet count for the next minute
124
+ return count_for_minute # Return the count for further use
155
125
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
126
+ def log_ten_minute_data (): #Logs data count for every 10 minutes and computes sampling rate and drift.
127
+ global total_data_received , last_ten_minute_time
159
128
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
129
+ print (f"Total data count after 10 minutes: { total_data_received } samples" ) # Print total data count
130
+ sampling_rate = total_data_received / (10 * 60 ) # Calculate the sampling rate
131
+ print (f"Sampling rate: { sampling_rate :.2f} samples/second" ) # Print the sampling rate
165
132
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
133
+ expected_sampling_rate = 250 # Expected sampling rate
134
+ drift = ((sampling_rate - expected_sampling_rate ) / expected_sampling_rate ) * 3600 # Calculate drift in seconds per hour
135
+ print (f"Drift: { drift :.2f} seconds/hour" ) # Print drift
170
136
171
- try :
172
- time .sleep (2 ) # Allow time for Arduino to initialize
137
+ total_data_received = 0 # Reset total data received for the next 10-minute interval
138
+ last_ten_minute_time = time .time () # Update the last ten-minute interval start
139
+
140
+ def parse_data (port , baudrate , lsl_flag = False , csv_flag = False ): # Main function to process data from the Arduino.
141
+ global total_packet_count , start_time , total_data_received , lsl_outlet , last_ten_minute_time
142
+
143
+ csv_writer = None # CSV writer is initially None
144
+ csv_filename = None
145
+
146
+ # Check if LSL stream is enabled
147
+ if lsl_flag :
148
+ lsl_stream_info = StreamInfo ('BioAmpDataStream' , 'EXG' , 6 , 250 , 'float32' , 'UpsideDownLabs' ) # Define LSL stream info
149
+ lsl_outlet = StreamOutlet (lsl_stream_info ) # Create LSL outlet
150
+ print ("LSL stream started" ) # Print message indicating LSL stream has started
151
+ time .sleep (0.5 ) # Delay to ensure that LSL stream setup is complete
152
+
153
+ # If CSV logging is requested
154
+ if csv_flag :
155
+ # Generate the filename dynamically based on current date and time
156
+ csv_filename = f"data_{ datetime .now ().strftime ('%Y-%m-%d_%H-%M-%S' )} .csv"
157
+ print (f"CSV recording started. Data will be saved to { csv_filename } " ) # Print CSV recording message
158
+
159
+ # Open the serial port and CSV file
160
+ with serial .Serial (port , baudrate , timeout = 0.1 ) as ser : # Open serial port
161
+ csv_file = open (csv_filename , mode = 'w' , newline = '' ) if csv_flag else None # Open CSV file if specified
162
+
163
+ if csv_file :
164
+ csv_writer = csv .writer (csv_file ) # Create CSV writer
165
+ csv_writer .writerow (['Counter' , 'Channel1' , 'Channel2' , 'Channel3' , 'Channel4' , 'Channel5' , 'Channel6' ]) # Write CSV header
173
166
174
- start_timer () # Initialize timer
167
+ # Delay to account for initial data and ensure accurate timing
168
+ start_timer ()
175
169
176
- while True :
177
- read_arduino_data (ser , csv_writer ) # Read data from Arduino
170
+ try :
171
+ while True :
172
+ read_arduino_data (ser , csv_writer ) # Read and process data from Arduino
178
173
179
- # if initial_samples_ignored:
180
- current_time = time .time () # Get current timestamp
174
+ current_time = time .time () # Get the current time
181
175
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
176
+ # Check if a minute has passed and log minute data
177
+ if current_time - start_time >= 60 :
178
+ total_data_received += log_minute_data () # Update total data received
179
+ start_time += 60 # Adjust the start time to handle next interval accurately
186
180
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
181
+ # Check if 10 minutes have passed and log ten -minute data
182
+ if current_time - last_ten_minute_time >= 600 :
183
+ total_data_received += log_minute_data () # Update total data received
184
+ log_ten_minute_data () # Log ten -minute data
185
+ start_timer () # Restart timers
192
186
193
- except KeyboardInterrupt :
194
- # Handle keyboard interrupt
195
- print (f"Exiting. \n Total missing samples: { missing_samples } " ) # Print missing samples and exit
187
+ except KeyboardInterrupt :
188
+ if csv_file :
189
+ csv_file .close () # Close CSV file
190
+ print (f"CSV recording stopped. Data saved to { csv_filename } ." ) # Print message indicating CSV recording stopped
191
+ print (f"Exiting. \n Total missing samples: { missing_samples } " ) # Print total missing samples
196
192
197
193
if __name__ == "__main__" :
194
+ # Argument parser for command-line interface
198
195
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
202
-
203
- args = parser .parse_args () # Parse command-line arguments
204
-
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." )
196
+ parser .add_argument ('-p' , '--port' , type = str , help = "Specify the COM port" )
197
+ parser .add_argument ('-b' , '--baudrate' , type = int , default = 57600 , help = "Set baud rate for the serial communication" )
198
+ parser .add_argument ('--csv' , action = 'store_true' , help = "Create and write to a CSV file" )
199
+ parser .add_argument ('--lsl' , action = 'store_true' , help = "Start LSL stream" )
200
+
201
+ args = parser .parse_args ()
202
+
203
+ # Determine port and start data parsing based on arguments
204
+ if args .lsl :
205
+ if args .port :
206
+ port = args .port # Use specified port
207
+ else :
208
+ port = auto_detect_arduino (baudrate = args .baudrate ) # Auto-detect Arduino port
209
+
210
+ if port is None :
211
+ print ("Arduino port not specified or detected. Exiting." ) # Print message if no port detected
212
+ else :
213
+ parse_data (port , args .baudrate , lsl_flag = args .lsl , csv_flag = args .csv ) # Start data parsing
212
214
else :
213
- parse_data ( port , args . baudrate ) # Start processing data
215
+ parser . print_help ( ) # Print help message if no valid arguments are provided
0 commit comments