Skip to content

Commit 4d22806

Browse files
authored
Merge pull request #13 from PayalLakra/bio_amptool
Update requirements.txt & "q" key press function
2 parents f91d7f2 + e3f594e commit 4d22806

File tree

5 files changed

+155
-65
lines changed

5 files changed

+155
-65
lines changed
File renamed without changes.

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# BioAmp Tool - Python
1+
# Chords - Python
22

3-
The BioAmp Tool is a Python script designed to interface with an Arduino-based bioamplifier, read data from it, optionally log this data to CSV or stream it via the Lab Streaming Layer (LSL), and visualize it through a graphical user interface (GUI) with live plotting.
3+
Chords Python script is designed to interface with an Arduino-based bioamplifier, read data from it, optionally log this data to CSV or stream it via the Lab Streaming Layer (LSL), and visualize it through a graphical user interface (GUI) with live plotting.
44

55
> [!NOTE]
66
> Flash Arduino code to your hardware from [Chords Arduino Firmware](https://github.com/upsidedownlabs/Chords-Arduino-Firmware) to use this python tool.
@@ -13,6 +13,7 @@ The BioAmp Tool is a Python script designed to interface with an Arduino-based b
1313
- **LSL Streaming:** Optionally streams data to an LSL outlet for integration with other software.
1414
- **Verbose Output:** Provides detailed statistics and error reporting, including sampling rate and drift.
1515
- **GUI:** Live plotting of six channels using a PyQt-based GUI.
16+
- **Timer:** Record data for a set time period in seconds.
1617

1718
## Requirements
1819

@@ -56,6 +57,7 @@ Then,
5657
- `--lsl`: Enable LSL streaming. Sends data to an LSL outlet.
5758
- `-v`, `--verbose`: Enable verbose output with detailed statistics and error reporting.
5859
- `--gui`: Enable the real-time data plotting GUI.
60+
- `-t` : Enable the timer to run program for a set time in seconds.
5961

6062
## Script Functions
6163

@@ -87,6 +89,10 @@ Parses data from Arduino and manages logging, streaming, and GUI updates.
8789
8890
Initializes and displays the GUI with six real-time plots, one for each bio-signal channel.
8991
92+
`cleanup()`
93+
94+
Handles all the cleanup tasks.
95+
9096
`main()`
9197
9298
Handles command-line argument parsing and initiates data processing.

bioamptool.py renamed to chords.py

Lines changed: 144 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
import numpy as np # For handling numeric arrays
3939
import pyqtgraph as pg # For real-time plotting
4040
from pyqtgraph.Qt import QtWidgets, QtCore # PyQt components for GUI
41-
import msvcrt #For keyboard interruptions
41+
import sys
42+
import signal
4243

4344
# Initialize global variables for tracking and processing data
4445
total_packet_count = 0 # Total packets received in the last second
@@ -50,10 +51,11 @@
5051
buffer = bytearray() # Buffer for storing incoming raw data from Arduino
5152
data = np.zeros((6, 2000)) # 2D array to store data for real-time plotting (6 channels, 2000 data points)
5253
samples_per_second = 0 # Number of samples received per second
54+
retry_limit = 4
5355

5456
# Initialize gloabal variables for Arduino Board
5557
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
5759

5860
# Initialize gloabal variables for Incoming Data
5961
PACKET_LENGTH = 16 # Expected length of each data packet
@@ -67,33 +69,55 @@
6769
lsl_outlet = None # Placeholder for LSL stream outlet
6870
verbose = False # Flag for verbose output mode
6971
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
7095

7196
# Function to automatically detect the Arduino's serial port
72-
def auto_detect_arduino(baudrate, timeout=1):
97+
def detect_hardware(baudrate, timeout=1):
7398
ports = serial.tools.list_ports.comports() # List available serial ports
99+
ser = None
74100
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
90105
return None # Return None if not found
91106

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+
92115
# Function to read data from Arduino
93116
def read_arduino_data(ser, csv_writer=None):
94117
global total_packet_count, cumulative_packet_count, previous_sample_number, missing_samples, buffer, data
95-
96118
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')
97121
buffer.extend(raw_data) # Add received data to the buffer
98122
while len(buffer) >= PACKET_LENGTH: # Continue processing if the buffer contains at least one full packet
99123
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):
105129
if len(buffer) >= sync_index + PACKET_LENGTH: # Check if a full packet is available
106130
packet = buffer[sync_index:sync_index + PACKET_LENGTH] # Extract the packet
107131
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+
108135
# Extract the packet if it is valid (correct length, sync bytes, and end byte)
109136
counter = packet[2] # Read the counter byte (for tracking sample order)
110137

@@ -199,7 +226,6 @@ def update():
199226
# Function to start timers for logging data
200227
def start_timer():
201228
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
203229
current_time = time.time() # Get the current time
204230
start_time = current_time # Set the start time for packet counting
205231
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):
221247
print(f"Total data count after 10 minutes: {cumulative_packet_count}") # Print cumulative data count
222248
sampling_rate = cumulative_packet_count / (10 * 60) # Calculate sampling rate
223249
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
225251
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift
226252
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
227253
cumulative_packet_count = 0 # Reset cumulative packet count
228254
last_ten_minute_time = time.time() # Update the last 10-minute interval start time
229255

230256
# 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):
232258
global total_packet_count, cumulative_packet_count, start_time, lsl_outlet, last_ten_minute_time, csv_filename
233259

234260
csv_writer = None # Placeholder for CSV writer
261+
csv_file = None
235262

236263
# Start LSL streaming if requested
237264
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
239266
lsl_outlet = StreamOutlet(lsl_stream_info) # Create LSL outlet
240267
print("LSL stream started") # Notify user
241-
time.sleep(0.5) # Wait for the LSL stream to start
242268

243-
# Start CSV logging if requested
244269
if csv_flag:
245270
csv_filename = f"data_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv" # Create timestamped filename
246271
print(f"CSV recording started. Data will be saved to {csv_filename}") # Notify user
272+
247273
# Initialize GUI if requested
248274
if gui_flag:
249275
init_gui() # Initialize GUI
250276
if lsl_flag:
251277
lsl_label.setText("LSL Status: Started") # Update LSL status in the GUI
252278
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
254280

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
258283
if csv_file:
259284
csv_writer = csv.writer(csv_file) # Create CSV writer
260285
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write header
261286

262-
start_timer() # Start timers for logging
287+
end_time = time.time() + run_time if run_time else None
288+
send_command(ser, 'START')
263289

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
270295
elapsed_since_last_10_minutes = current_time - last_ten_minute_time # Time elapsed since the last 10-minute interval
271296

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+
277304
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')
283310
break
284311

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()
288368

289-
finally:
290-
if csv_file:
291-
csv_file.close()
292-
print(f"CSV recording saved as {csv_filename}")
293-
print(f"Exiting.\nTotal missing samples: {missing_samples}") # Print final missing samples count
294-
295369
# Main entry point of the script
296370
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
299373
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
301375
parser.add_argument('--csv', action='store_true', help="Create and write to a CSV file") # CSV logging flag
302376
parser.add_argument('--lsl', action='store_true', help="Start LSL stream") # LSL streaming flag
303377
parser.add_argument('--gui', action='store_true', help="Start GUI for real-time plotting") # GUI flag
304378
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
305380

306381
args = parser.parse_args() # Parse command-line arguments
307382
verbose = args.verbose # Set verbose mode
308383

384+
# Register the signal handler to handle Ctrl+C
385+
signal.signal(signal.SIGINT, signal_handler)
386+
309387
# Check if any logging or GUI options are selected, else show help
310388
if not args.csv and not args.lsl and not args.gui:
311389
parser.print_help() # Print help if no options are selected
312390
return
313391

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:
316399
print("Arduino port not specified or detected. Exiting.") # Notify if no port is available
317400
return
318401

319402
# 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)
321404

322405
# Run the main function if this script is executed
323406
if __name__ == "__main__":
File renamed without changes.

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
numpy==2.1.1
1+
numpy==1.26.4
22
pylsl==1.16.2
33
pyqtgraph==0.13.7
44
pyserial==3.5
5-
PySide2==5.15.2.1
5+
PySide2==5.15.2.1
6+
keyboard==0.13.5

0 commit comments

Comments
 (0)