38
38
import numpy as np # For handling numeric arrays
39
39
import pyqtgraph as pg # For real-time plotting
40
40
from pyqtgraph .Qt import QtWidgets , QtCore # PyQt components for GUI
41
- import msvcrt #For keyboard interruptions
41
+ import sys
42
+ import signal
42
43
43
44
# Initialize global variables for tracking and processing data
44
45
total_packet_count = 0 # Total packets received in the last second
50
51
buffer = bytearray () # Buffer for storing incoming raw data from Arduino
51
52
data = np .zeros ((6 , 2000 )) # 2D array to store data for real-time plotting (6 channels, 2000 data points)
52
53
samples_per_second = 0 # Number of samples received per second
54
+ retry_limit = 4
53
55
54
56
# Initialize gloabal variables for Arduino Board
55
57
board = "" # Variable for Connected Arduino Board
56
- boards_sample_rate = {"UNO-R3" :250 , "UNO-R4" :500 } #Standard Sample rate for Arduino Boards Different Firmware
58
+ supported_boards = {"UNO-R3" :250 , "UNO-R4" :500 } #Supported boards and their sampling rate
57
59
58
60
# Initialize gloabal variables for Incoming Data
59
61
PACKET_LENGTH = 16 # Expected length of each data packet
67
69
lsl_outlet = None # Placeholder for LSL stream outlet
68
70
verbose = False # Flag for verbose output mode
69
71
csv_filename = None # Store CSV filename
72
+ csv_file = None
73
+ ser = None
74
+
75
+ def connect_hardware (port , baudrate , timeout = 1 ):
76
+ try :
77
+ ser = serial .Serial (port , baudrate = baudrate , timeout = timeout ) # Try opening the port
78
+ response = None
79
+ retry_counter = 0
80
+ while response is None or retry_counter < retry_limit :
81
+ ser .write (b'WHORU\n ' ) # Check board type
82
+ response = ser .readline ().strip ().decode () # Try reading from the port
83
+ retry_counter += 1
84
+ if response in supported_boards : # If response is received, assume it's the Arduino
85
+ global board
86
+ board = response # Set board type
87
+ print (f"{ response } detected at { port } " ) # Notify the user
88
+ if ser is not None :
89
+ return ser # Return the port name
90
+ ser .close () # Close the port if no response
91
+ except (OSError , serial .SerialException ): # Handle exceptions if the port can't be opened
92
+ pass
93
+ print ("Unable to connect to any hardware!" ) # Notify if no Arduino is found
94
+ return None # Return None if not found
70
95
71
96
# Function to automatically detect the Arduino's serial port
72
- def auto_detect_arduino (baudrate , timeout = 1 ):
97
+ def detect_hardware (baudrate , timeout = 1 ):
73
98
ports = serial .tools .list_ports .comports () # List available serial ports
99
+ ser = None
74
100
for port in ports : # Iterate through each port
75
- try :
76
- ser = serial .Serial (port .device , baudrate = baudrate , timeout = timeout ) # Try opening the port
77
- ser .write (b'WHORU\n ' )
78
- time .sleep (1 ) # Wait for the device to initialize
79
- response = ser .readline ().strip ().decode () # Try reading from the port
80
- if response : # If response is received, assume it's the Arduino
81
- global board
82
- ser .close () # Close the serial connection
83
- print (f"{ response } detected at { port .device } " ) # Notify the user
84
- board = response
85
- return port .device # Return the port name
86
- ser .close () # Close the port if no response
87
- except (OSError , serial .SerialException ): # Handle exceptions if the port can't be opened
88
- pass
89
- print ("Arduino not detected" ) # Notify if no Arduino is found
101
+ ser = connect_hardware (port .device , baudrate )
102
+ if ser is not None :
103
+ return ser
104
+ print ("Unable to detect hardware!" ) # Notify if no Arduino is found
90
105
return None # Return None if not found
91
106
107
+ def send_command (ser , command ):
108
+ ser .flushInput () # Clear the input buffer
109
+ ser .flushOutput () # Clear the output buffer
110
+ ser .write (f"{ command } \n " .encode ()) # Send command
111
+ time .sleep (0.1 ) # Wait briefly to ensure Arduino processes the command
112
+ response = ser .readline ().decode ('utf-8' , errors = 'ignore' ).strip () # Read response
113
+ return response
114
+
92
115
# Function to read data from Arduino
93
116
def read_arduino_data (ser , csv_writer = None ):
94
117
global total_packet_count , cumulative_packet_count , previous_sample_number , missing_samples , buffer , data
95
-
96
118
raw_data = ser .read (ser .in_waiting or 1 ) # Read available data from the serial port
119
+ if raw_data == b'' :
120
+ send_command (ser , 'START' )
97
121
buffer .extend (raw_data ) # Add received data to the buffer
98
122
while len (buffer ) >= PACKET_LENGTH : # Continue processing if the buffer contains at least one full packet
99
123
sync_index = buffer .find (bytes ([SYNC_BYTE1 , SYNC_BYTE2 ])) # Search for the sync marker
@@ -105,6 +129,9 @@ def read_arduino_data(ser, csv_writer=None):
105
129
if len (buffer ) >= sync_index + PACKET_LENGTH : # Check if a full packet is available
106
130
packet = buffer [sync_index :sync_index + PACKET_LENGTH ] # Extract the packet
107
131
if len (packet ) == PACKET_LENGTH and packet [0 ] == SYNC_BYTE1 and packet [1 ] == SYNC_BYTE2 and packet [- 1 ] == END_BYTE :
132
+ if (start_time is None ):
133
+ start_timer () # Start timers for logging
134
+
108
135
# Extract the packet if it is valid (correct length, sync bytes, and end byte)
109
136
counter = packet [2 ] # Read the counter byte (for tracking sample order)
110
137
@@ -199,7 +226,6 @@ def update():
199
226
# Function to start timers for logging data
200
227
def start_timer ():
201
228
global start_time , last_ten_minute_time , total_packet_count , cumulative_packet_count
202
- time .sleep (0.5 ) # Give some time to settle before starting
203
229
current_time = time .time () # Get the current time
204
230
start_time = current_time # Set the start time for packet counting
205
231
last_ten_minute_time = current_time # Set the start time for 10-minute interval logging
@@ -221,103 +247,160 @@ def log_ten_minute_data(verbose=False):
221
247
print (f"Total data count after 10 minutes: { cumulative_packet_count } " ) # Print cumulative data count
222
248
sampling_rate = cumulative_packet_count / (10 * 60 ) # Calculate sampling rate
223
249
print (f"Sampling rate: { sampling_rate :.2f} samples/second" ) # Print sampling rate
224
- expected_sampling_rate = boards_sample_rate [board ] # Expected sampling rate
250
+ expected_sampling_rate = supported_boards [board ] # Expected sampling rate
225
251
drift = ((sampling_rate - expected_sampling_rate ) / expected_sampling_rate ) * 3600 # Calculate drift
226
252
print (f"Drift: { drift :.2f} seconds/hour" ) # Print drift
227
253
cumulative_packet_count = 0 # Reset cumulative packet count
228
254
last_ten_minute_time = time .time () # Update the last 10-minute interval start time
229
255
230
256
# Main function to parse command-line arguments and handle data acquisition
231
- def parse_data (port , baudrate , lsl_flag = False , csv_flag = False , gui_flag = False , verbose = False ):
257
+ def parse_data (ser , lsl_flag = False , csv_flag = False , gui_flag = False , verbose = False , run_time = None ):
232
258
global total_packet_count , cumulative_packet_count , start_time , lsl_outlet , last_ten_minute_time , csv_filename
233
259
234
260
csv_writer = None # Placeholder for CSV writer
261
+ csv_file = None
235
262
236
263
# Start LSL streaming if requested
237
264
if lsl_flag :
238
- lsl_stream_info = StreamInfo ('BioAmpDataStream' , 'EXG' , 6 , boards_sample_rate [board ], 'float32' , 'UpsideDownLabs' ) # Define LSL stream info
265
+ lsl_stream_info = StreamInfo ('BioAmpDataStream' , 'EXG' , 6 , supported_boards [board ], 'float32' , 'UpsideDownLabs' ) # Define LSL stream info
239
266
lsl_outlet = StreamOutlet (lsl_stream_info ) # Create LSL outlet
240
267
print ("LSL stream started" ) # Notify user
241
- time .sleep (0.5 ) # Wait for the LSL stream to start
242
268
243
- # Start CSV logging if requested
244
269
if csv_flag :
245
270
csv_filename = f"data_{ datetime .now ().strftime ('%Y-%m-%d_%H-%M-%S' )} .csv" # Create timestamped filename
246
271
print (f"CSV recording started. Data will be saved to { csv_filename } " ) # Notify user
272
+
247
273
# Initialize GUI if requested
248
274
if gui_flag :
249
275
init_gui () # Initialize GUI
250
276
if lsl_flag :
251
277
lsl_label .setText ("LSL Status: Started" ) # Update LSL status in the GUI
252
278
if csv_flag :
253
- csv_label .setText (f"CSV Recording: { csv_filename } " ) # Update CSV status in the GUI
279
+ csv_label .setText (f"CSV Recording: { csv_filename } " ) # Update CSV status in the GUI
254
280
255
- # Open serial connection
256
- with serial .Serial (port , baudrate , timeout = 0.1 ) as ser :
257
- csv_file = open (csv_filename , mode = 'w' , newline = '' ) if csv_flag else None # Open CSV file if logging is enabled
281
+ try :
282
+ csv_file = open (csv_filename , mode = 'w' , newline = '' ) if csv_flag else None # Open CSV file if logging is
258
283
if csv_file :
259
284
csv_writer = csv .writer (csv_file ) # Create CSV writer
260
285
csv_writer .writerow (['Counter' , 'Channel1' , 'Channel2' , 'Channel3' , 'Channel4' , 'Channel5' , 'Channel6' ]) # Write header
261
286
262
- start_timer () # Start timers for logging
287
+ end_time = time .time () + run_time if run_time else None
288
+ send_command (ser , 'START' )
263
289
264
- try :
265
- ser .write (b'START\n ' )
266
- while True :
267
- read_arduino_data (ser , csv_writer ) # Read and process data from Arduino
268
- current_time = time .time () # Get the current time
269
- elapsed_time = current_time - start_time # Time elapsed since the last second
290
+ while True :
291
+ read_arduino_data (ser , csv_writer ) # Read and process data from Arduino
292
+ if (start_time is not None ):
293
+ current_time = time .time () # Get the current time
294
+ elapsed_time = current_time - start_time # Time elapsed since the last second
270
295
elapsed_since_last_10_minutes = current_time - last_ten_minute_time # Time elapsed since the last 10-minute interval
271
296
272
- if elapsed_time >= 1 : # Check if one second has passed
273
- log_one_second_data (verbose ) # Log data for the last second
274
- start_time = current_time # Reset the start time for the next second
275
- if elapsed_since_last_10_minutes >= 600 : # Check if 10 minutes have passed
276
- log_ten_minute_data (verbose ) # Log data for the last 10 minutes
297
+ if elapsed_time >= 1 :
298
+ log_one_second_data (verbose )
299
+ start_time = current_time
300
+
301
+ if elapsed_since_last_10_minutes >= 600 :
302
+ log_ten_minute_data (verbose )
303
+
277
304
if gui_flag :
278
- QtWidgets .QApplication .processEvents () # Process GUI events if GUI is enabled
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" )
305
+ QtWidgets .QApplication .processEvents ()
306
+
307
+ if run_time and current_time >= end_time :
308
+ print ( "Runtime Over, sending STOP command..." )
309
+ send_command ( ser , 'STOP' )
283
310
break
284
311
285
- except KeyboardInterrupt :
286
- ser .write (b'STOP\n ' )
287
- print ("Process interrupted by user" )
312
+ except KeyboardInterrupt :
313
+ print ("Process interrupted by user" )
314
+
315
+ finally :
316
+ cleanup ()
317
+
318
+ print (f"Total missing samples: { missing_samples } " )
319
+ sys .exit (0 )
320
+
321
+ def cleanup ():
322
+ global qApp , ser , lsl_outlet , csv_file
323
+
324
+ # Close the serial connection first
325
+ try :
326
+ if ser is not None and ser .is_open :
327
+ send_command (ser , 'STOP' ) # Ensure the STOP command is sent
328
+ time .sleep (1 )
329
+ ser .reset_input_buffer () # Clear the input buffer
330
+ ser .reset_output_buffer () # Clear the output buffer
331
+ ser .close () # Close the serial port
332
+ print ("Serial connection closed." )
333
+ else :
334
+ print ("Serial connection is not open." )
335
+ except Exception as e :
336
+ print (f"Error while closing serial connection: { e } " )
337
+
338
+ # Close the LSL stream if it exists
339
+ try :
340
+ if lsl_outlet :
341
+ print ("Closing LSL Stream." )
342
+ lsl_outlet = None # Cleanup LSL outlet
343
+ except Exception as e :
344
+ print (f"Error while closing LSL stream: { e } " )
345
+
346
+ # Close the CSV file if it exists
347
+ try :
348
+ if csv_file :
349
+ csv_file .close () # Close the CSV file
350
+ print ("CSV recording saved." )
351
+ except Exception as e :
352
+ print (f"Error while closing CSV file: { e } " )
353
+
354
+ # Close the GUI if it exists
355
+ try :
356
+ if qApp :
357
+ print ("Closing the GUI." )
358
+ qApp .quit () # Close the PyQt application
359
+ except Exception as e :
360
+ print (f"Error while closing the GUI: { e } " )
361
+
362
+ print ("Cleanup completed, exiting program." )
363
+ print (f"Total missing samples: { missing_samples } " )
364
+ sys .exit (0 )
365
+
366
+ def signal_handler (sig , frame ):
367
+ cleanup ()
288
368
289
- finally :
290
- if csv_file :
291
- csv_file .close ()
292
- print (f"CSV recording saved as { csv_filename } " )
293
- print (f"Exiting.\n Total missing samples: { missing_samples } " ) # Print final missing samples count
294
-
295
369
# Main entry point of the script
296
370
def main ():
297
- global verbose
298
- parser = argparse .ArgumentParser (description = "Upside Down Labs - BioAmp Tool" ) # Create argument parser
371
+ global verbose , ser
372
+ parser = argparse .ArgumentParser (description = "Upside Down Labs - BioAmp Tool" , allow_abbrev = False ) # Create argument parser
299
373
parser .add_argument ('-p' , '--port' , type = str , help = "Specify the COM port" ) # Port argument
300
- parser .add_argument ('-b' , '--baudrate' , type = int , default = 57600 , help = "Set baud rate for the serial communication" ) # Baud rate
374
+ parser .add_argument ('-b' , '--baudrate' , type = int , default = 230400 , help = "Set baud rate for the serial communication" ) # Baud rate
301
375
parser .add_argument ('--csv' , action = 'store_true' , help = "Create and write to a CSV file" ) # CSV logging flag
302
376
parser .add_argument ('--lsl' , action = 'store_true' , help = "Start LSL stream" ) # LSL streaming flag
303
377
parser .add_argument ('--gui' , action = 'store_true' , help = "Start GUI for real-time plotting" ) # GUI flag
304
378
parser .add_argument ('-v' , '--verbose' , action = 'store_true' , help = "Enable verbose output with statistical data" ) # Verbose flag
379
+ parser .add_argument ('-t' , '--time' , type = int , help = "Run the program for a specified number of seconds and then exit" ) #set time
305
380
306
381
args = parser .parse_args () # Parse command-line arguments
307
382
verbose = args .verbose # Set verbose mode
308
383
384
+ # Register the signal handler to handle Ctrl+C
385
+ signal .signal (signal .SIGINT , signal_handler )
386
+
309
387
# Check if any logging or GUI options are selected, else show help
310
388
if not args .csv and not args .lsl and not args .gui :
311
389
parser .print_help () # Print help if no options are selected
312
390
return
313
391
314
- port = args .port or auto_detect_arduino (args .baudrate ) # Get the port from arguments or auto-detect
315
- if port is None :
392
+ if args .port :
393
+ print ("trying to connect to port:" , args .port )
394
+ ser = connect_hardware (port = args .port , baudrate = args .baudrate )
395
+ else :
396
+ ser = detect_hardware (baudrate = args .baudrate )
397
+
398
+ if ser is None :
316
399
print ("Arduino port not specified or detected. Exiting." ) # Notify if no port is available
317
400
return
318
401
319
402
# Start data acquisition
320
- parse_data (port , args . baudrate , lsl_flag = args .lsl , csv_flag = args .csv , gui_flag = args .gui , verbose = args .verbose )
403
+ parse_data (ser , lsl_flag = args .lsl , csv_flag = args .csv , gui_flag = args .gui , verbose = args .verbose , run_time = args . time )
321
404
322
405
# Run the main function if this script is executed
323
406
if __name__ == "__main__" :
0 commit comments