26
26
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
27
# SOFTWARE.
28
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
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
36
37
37
38
# 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
58
57
for port in ports :
59
58
try :
60
59
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
63
62
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
66
65
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
68
67
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
71
70
return None # Return None if no Arduino is detected
72
71
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
80
76
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
84
79
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
87
82
continue
88
83
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
91
86
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
93
88
94
- # Ensure counter number is exactly one more than the previous one
95
89
if previous_sample_number is not None and counter != (previous_sample_number + 1 ) % 256 :
90
+ # Check for missing samples based on the counter
96
91
missing_samples += (counter - previous_sample_number - 1 ) % 256
97
92
print (f"Error: Expected counter { previous_sample_number + 1 } but received { counter } . Missing samples: { missing_samples } " )
98
93
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
110
97
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
113
104
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
116
109
117
- del buffer [:sync_index + PACKET_LENGTH ]
110
+ del buffer [:sync_index + PACKET_LENGTH ] # Remove processed packet from the buffer
118
111
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
134
125
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
155
133
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
159
137
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
165
141
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
170
144
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
173
148
174
- start_timer () # Initialize timer
149
+ verbose_mode = verbose_flag # Set the verbose mode flag
175
150
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
178
153
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
181
164
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
186
172
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
192
175
193
- except KeyboardInterrupt :
194
- # Handle keyboard interrupt
195
- print (f"Exiting. \n Total 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. \n Total missing samples: { missing_samples } " ) # Print total missing samples
196
206
197
207
if __name__ == "__main__" :
208
+ # Argument parser for command-line interface
198
209
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" )
202
215
203
216
args = parser .parse_args () # Parse command-line arguments
204
217
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
212
228
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