Skip to content

Commit 96bdaf2

Browse files
authored
Merge pull request #32 from PayalLakra/bio_amptool
Requirements updates, Chords baud rate update, ffteeg- moving window implementation
2 parents eb83aab + c6b3b6a commit 96bdaf2

File tree

8 files changed

+216
-179
lines changed

8 files changed

+216
-179
lines changed

README.md

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ To use the script, run it from the command line with various options:
5151
### Options
5252

5353
- `-p`, `--port` <port>: Specify the serial port to use (e.g., COM5, /dev/ttyUSB0).
54-
- `-b`, `--baudrate` <baudrate>: Set the baud rate for serial communication (default is 230400).
54+
- `-b`, `--baudrate` <baudrate>: Set the baud rate for serial communication. By default the script will first attempt to use 230400, and if that fails, it will automatically fallback to 115200.
5555
- `--csv`: Enable CSV logging. Data will be saved to a timestamped file.
5656
- `--lsl`: Enable LSL streaming. Sends data to an LSL outlet.
5757
- `-v`, `--verbose`: Enable verbose output with detailed statistics and error reporting.
@@ -66,38 +66,8 @@ To use the script, run it from the command line with various options:
6666

6767
- **Log Intervals**: The script logs data counts every second and provides a summary every 10 minutes, including the sampling rate and drift in seconds per hour.
6868

69-
### LSL Streaming
70-
71-
- **Stream Name**: `BioAmpDataStream`
72-
- **Stream Type**: `EXG`
73-
- **Channel Count**: `6`
74-
- **Sampling Rate**: `UNO-R3 : 250 Hz` , `UNO-R4 : 500 Hz`
75-
- **Data Format**: `float32`
76-
77-
78-
### Script Functions
79-
80-
`auto_detect_arduino(baudrate, timeout=1)`: Detects an Arduino connected via serial port. Returns the port name if detected.
81-
82-
`read_arduino_data(ser, csv_writer=None)`: Reads and processes data from the Arduino. Writes data to CSV and/or LSL stream if enabled.
83-
84-
`start_timer()`: Initializes timers for 1-second and 10-minute intervals.
85-
86-
`log_one_second_data(verbose=False)`: Logs and resets data for the 1-second interval.
87-
88-
`log_ten_minute_data(verbose=False)`: Logs data and statistics for the 10-minute interval.
89-
90-
`parse_data(port,baudrate,lsl_flag=False,csv_flag=False,verbose=False)`: Parses data from Arduino and manages logging, streaming, and GUI updates.
91-
92-
`cleanup()`: Handles all the cleanup tasks.
93-
94-
`main()`: Handles command-line argument parsing and initiates data processing.
95-
9669
## Applications
9770

98-
> [!IMPORTANT]
99-
Before using the below Applications make sure you are in application folder.
100-
10171
### GUI
10272

10373
- `python gui.py`: Enable the real-time data plotting GUI.
@@ -108,7 +78,7 @@ To use the script, run it from the command line with various options:
10878

10979
### HEART RATE
11080

111-
- `python heartbeat.ecg.py`:Enable a GUI with real-time ECG and heart rate.
81+
- `python heartbeat_ecg.py`:Enable a GUI with real-time ECG and heart rate.
11282

11383
### EMG ENVELOPE
11484

@@ -122,6 +92,38 @@ To use the script, run it from the command line with various options:
12292

12393
- `python ffteeg.py`: Enable a GUI with real-time EEG data with its FFT.
12494

95+
### Keystroke
96+
97+
- `python keystroke.py`: On running, a pop-up opens for connecting, and on pressing Start, blinks are detected to simulate spacebar key presses.
98+
99+
## Running All Applications Together
100+
101+
To run all applications together:
102+
103+
```bash
104+
python app.py
105+
```
106+
107+
> [!NOTE]
108+
> Before running, make sure to install all dependencies by running the command:
109+
```bash
110+
pip install -r app_requirements.txt
111+
```
112+
113+
This will launch a Web interface. Use the interface to control the applications:
114+
115+
1. Click the `Start LSL Stream` button to initiate the LSL stream.
116+
2. Then, click on any application button to run the desired module.
117+
118+
### Available Applications
119+
- `ffteeg`: Real-time EEG analysis with FFT and brainwave power calculation.
120+
- `heartbeat_ecg`: Analyze ECG data and extract heartbeat metrics.
121+
- `eog`: Real-time EOG monitoring with blink detection.
122+
- `emgenvelope`: Real-time EMG monitor with filtering and RMS envelope.
123+
- `keystroke`: GUI for EOG-based blink detection triggering a keystroke.
124+
- `game`: Launch an EEG game for 2 players (Tug of War).
125+
- `csv_plotter`: Plot data from a CSV file.
126+
- `gui`: Launch the GUI for real time signal visualization.
125127

126128
## Troubleshooting
127129

app_requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ neurokit2==0.2.10
77
plotly==5.24.1
88
pandas==2.2.3
99
tk==0.1.0
10-
PyAutoGUI==0.9.54
10+
PyAutoGUI==0.9.54
11+
Flask==3.1.0
12+
psutil==6.1.1

chords.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,17 @@
5454
board = "" # Variable for Connected Arduino Board
5555
supported_boards = {
5656
"UNO-R3": {"sampling_rate": 250, "Num_channels": 6},
57-
"UNO-CLONE": {"sampling_rate": 250, "Num_channels": 6}, # Baud Rate 115200
57+
"UNO-CLONE": {"sampling_rate": 250, "Num_channels": 6},
58+
"GENUINO-UNO": {"sampling_rate": 250, "Num_channels": 6},
5859
"UNO-R4": {"sampling_rate": 500, "Num_channels": 6},
5960
"RPI-PICO-RP2040": {"sampling_rate": 500, "Num_channels": 3},
60-
"NANO-CLONE": {"sampling_rate": 250, "Num_channels": 8}, # Baud Rate 115200
61+
"NANO-CLONE": {"sampling_rate": 250, "Num_channels": 8},
62+
"NANO-CLASSIC": {"sampling_rate": 250, "Num_channels": 8},
63+
"STM32F4-BLACK-PILL": {"sampling_rate": 500, "Num_channels": 8},
64+
"STM32G4-CORE-BOARD": {"sampling_rate": 500, "Num_channels": 16},
65+
"MEGA-2560-R3": {"sampling_rate": 250, "Num_channels": 16},
66+
"MEGA-2560-CLONE": {"sampling_rate": 250, "Num_channels": 16},
67+
"GIGA-R1": {"sampling_rate": 500, "Num_channels": 6},
6168
}
6269

6370
# Initialize gloabal variables for Incoming Data
@@ -80,14 +87,18 @@ def connect_hardware(port, baudrate, timeout=1):
8087
ser = serial.Serial(port, baudrate=baudrate, timeout=timeout) # Try opening the port
8188
response = None
8289
retry_counter = 0
83-
while response is None or retry_counter < retry_limit:
90+
while response not in supported_boards and retry_counter < retry_limit:
8491
ser.write(b'WHORU\n') # Check board type
85-
response = ser.readline().strip().decode() # Try reading from the port
92+
try:
93+
response = ser.readline().strip().decode() # Attempt to decode the response
94+
except UnicodeDecodeError as e:
95+
print(f"Decode error: {e}. Ignoring this response.")
96+
response = None
8697
retry_counter += 1
8798
if response in supported_boards: # If response is received, assume it's the Arduino
8899
global board, sampling_rate, data, num_channels, packet_length
89100
board = response # Set board type
90-
print(f"{response} detected at {port}") # Notify the user
101+
print(f"{response} detected at {port} with baudrate {baudrate}.") # Notify the user
91102
sampling_rate = supported_boards[board]["sampling_rate"]
92103
num_channels = supported_boards[board]["Num_channels"]
93104
packet_length = (2 * num_channels) + HEADER_LENGTH + 1
@@ -97,17 +108,19 @@ def connect_hardware(port, baudrate, timeout=1):
97108
ser.close() # Close the port if no response
98109
except (OSError, serial.SerialException): # Handle exceptions if the port can't be opened
99110
pass
100-
print("Unable to connect to any hardware!") # Notify if no Arduino is found
111+
print(f"Unable to connect to any hardware at baudrate {baudrate}") # Notify if no Arduino is found
101112
return None # Return None if not found
102113

103-
# Function to automatically detect the Arduino's serial port
104-
def detect_hardware(baudrate, timeout=1):
114+
def detect_hardware(baudrate=None, timeout=1):
105115
ports = serial.tools.list_ports.comports() # List available serial ports
106116
ser = None
117+
baudrates = [baudrate] if baudrate else [230400, 115200]
107118
for port in ports: # Iterate through each port
108-
ser = connect_hardware(port.device, baudrate)
109-
if ser is not None:
110-
return ser
119+
for baud_rate in baudrates: # Iterate through all baud rates
120+
print(f"Trying {port.device} at Baudrate {baud_rate}...")
121+
ser = connect_hardware(port.device, baud_rate, timeout)
122+
if ser is not None:
123+
return ser
111124
print("Unable to detect hardware!") # Notify if no Arduino is found
112125
return None # Return None if not found
113126

@@ -157,7 +170,7 @@ def read_arduino_data(ser, csv_writer=None, inverted=False):
157170
total_packet_count += 1 # Increment total packet count for the current second
158171
cumulative_packet_count += 1 # Increment cumulative packet count for the last 10 minutes
159172

160-
# Extract channel data (6 channels, 2 bytes per channel)
173+
# Extract channel data (num_channels, 2 bytes per channel)
161174
channel_data = []
162175
for channel in range(num_channels): # Loop through channel data bytes
163176
high_byte = packet[2*channel + HEADER_LENGTH]
@@ -241,7 +254,7 @@ def parse_data(ser, lsl_flag=False, csv_flag=False, verbose=False, run_time=None
241254
csv_writer.writerow([f"Arduino Board: {board}"])
242255
csv_writer.writerow([f"Sampling Rate (samples per second): {supported_boards[board]['sampling_rate']}"])
243256
csv_writer.writerow([]) # Blank row for separation
244-
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write header
257+
csv_writer.writerow(['Counter'] + [f'Channel{i+1}' for i in range(num_channels)]) # Write header
245258

246259
end_time = time.time() + run_time if run_time else None
247260
send_command(ser, 'START')
@@ -319,7 +332,7 @@ def main():
319332
global verbose,ser
320333
parser = argparse.ArgumentParser(description="Upside Down Labs - Chords-Python Tool",allow_abbrev = False) # Create argument parser
321334
parser.add_argument('-p', '--port', type=str, help="Specify the COM port") # Port argument
322-
parser.add_argument('-b', '--baudrate', type=int, default=230400, help="Set baud rate for the serial communication") # Baud rate
335+
parser.add_argument('-b', '--baudrate', type=int, help="Set baud rate for the serial communication") # Baud rate
323336
parser.add_argument('--csv', action='store_true', help="Create and write to a CSV file") # CSV logging flag
324337
parser.add_argument('--lsl', action='store_true', help="Start LSL stream") # LSL streaming flag
325338
parser.add_argument('-v', '--verbose', action='store_true', help="Enable verbose output with statistical data") # Verbose flag

ffteeg.py

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self):
2727
self.eeg_plot_widget.showGrid(x=True, y=True)
2828
self.eeg_plot_widget.setLabel('bottom', 'EEG Plot')
2929
self.eeg_plot_widget.setYRange(-5000, 5000, padding=0)
30-
self.eeg_plot_widget.setXRange(0, 2, padding=0)
30+
self.eeg_plot_widget.setXRange(0, 4, padding=0)
3131
self.eeg_plot_widget.setMouseEnabled(x=False, y=True) # Disable zoom
3232
self.main_layout.addWidget(self.eeg_plot_widget)
3333

@@ -39,9 +39,9 @@ def __init__(self):
3939
self.fft_plot.setBackground('w')
4040
self.fft_plot.showGrid(x=True, y=True)
4141
self.fft_plot.setLabel('bottom', 'FFT')
42-
# self.fft_plot.setYRange(0, 25000, padding=0)
42+
# self.fft_plot.setYRange(0, 500, padding=0)
4343
self.fft_plot.setXRange(0, 50, padding=0) # Set x-axis to 0 to 50 Hz
44-
self.fft_plot.setMouseEnabled(x=False, y=False) # Disable zoom
44+
# self.fft_plot.setMouseEnabled(x=False, y=False) # Disable zoom
4545
self.fft_plot.setAutoVisible(y=True) # Allow y-axis to autoscale
4646
self.bottom_layout.addWidget(self.fft_plot)
4747

@@ -74,13 +74,8 @@ def __init__(self):
7474
print(f"Sampling rate: {self.sampling_rate} Hz")
7575

7676
# Data and Buffers
77-
self.one_second_buffer = deque(maxlen=self.sampling_rate) # 1-second buffer
78-
self.buffer_size = self.sampling_rate * 10
79-
self.moving_window_size = self.sampling_rate * 2 # 2-second window
80-
81-
self.eeg_data = np.zeros(self.buffer_size)
82-
self.time_data = np.linspace(0, 10, self.buffer_size)
83-
self.current_index = 0
77+
self.eeg_data = deque(maxlen=500) # Initialize moving window with 500 samples
78+
self.moving_window = deque(maxlen=500) # 500 samples for FFT and power calculation (sliding window)
8479

8580
self.b_notch, self.a_notch = iirnotch(50, 30, self.sampling_rate)
8681
self.b_band, self.a_band = butter(4, [0.5 / (self.sampling_rate / 2), 48.0 / (self.sampling_rate / 2)], btype='band')
@@ -93,7 +88,7 @@ def __init__(self):
9388
self.timer.timeout.connect(self.update_plot)
9489
self.timer.start(20)
9590

96-
self.eeg_curve = self.eeg_plot_widget.plot(self.time_data, self.eeg_data, pen=pg.mkPen('b', width=1)) #EEG Colour is blue
91+
self.eeg_curve = self.eeg_plot_widget.plot(pen=pg.mkPen('b', width=1))
9792
self.fft_curve = self.fft_plot.plot(pen=pg.mkPen('r', width=1)) # FFT Colour is red
9893

9994
def update_plot(self):
@@ -106,32 +101,26 @@ def update_plot(self):
106101
band_filtered, self.zi_band = lfilter(self.b_band, self.a_band, notch_filtered, zi=self.zi_band)
107102
band_filtered = band_filtered[-1] # Get the current filtered point
108103

109-
# Update the EEG plot
110-
self.eeg_data[self.current_index] = band_filtered
111-
self.current_index = (self.current_index + 1) % self.buffer_size
104+
# Update EEG data buffer
105+
self.eeg_data.append(band_filtered)
112106

113-
if self.current_index == 0:
114-
plot_data = self.eeg_data
107+
if len(self.moving_window) < 500:
108+
self.moving_window.append(band_filtered)
115109
else:
116-
plot_data = np.concatenate((self.eeg_data[self.current_index:], self.eeg_data[:self.current_index]))
110+
self.process_fft_and_brainpower()
117111

118-
recent_data = plot_data[-self.moving_window_size:]
119-
recent_time = np.linspace(0, len(recent_data) / self.sampling_rate, len(recent_data))
120-
self.eeg_curve.setData(recent_time, recent_data)
112+
self.moving_window = deque(list(self.moving_window)[50:] + [band_filtered], maxlen=500)
121113

122-
self.one_second_buffer.append(band_filtered) # Add the filtered point to the 1-second buffer
123-
if len(self.one_second_buffer) == self.sampling_rate: # Process FFT and brainwave power
124-
self.process_fft_and_brainpower()
125-
self.one_second_buffer.clear()
126-
127-
def process_fft_and_brainpower(self):
128-
window = np.hanning(len(self.one_second_buffer)) # Apply Hanning window to the buffer
129-
buffer_windowed = np.array(self.one_second_buffer) * window
114+
plot_data = np.array(self.eeg_data)
115+
time_axis = np.linspace(0, 4, len(plot_data))
116+
self.eeg_curve.setData(time_axis, plot_data)
130117

131-
# Perform FFT
132-
fft_result = np.abs(fft(buffer_windowed))[:len(buffer_windowed) // 2]
118+
def process_fft_and_brainpower(self):
119+
window = np.hanning(len(self.moving_window))
120+
buffer_windowed = np.array(self.moving_window) * window
121+
fft_result = np.abs(np.fft.rfft(buffer_windowed))
133122
fft_result /= len(buffer_windowed)
134-
freqs = np.fft.fftfreq(len(buffer_windowed), 1 / self.sampling_rate)[:len(buffer_windowed) // 2]
123+
freqs = np.fft.rfftfreq(len(buffer_windowed), 1 / self.sampling_rate)
135124
self.fft_curve.setData(freqs, fft_result)
136125

137126
brainwave_power = self.calculate_brainwave_power(fft_result, freqs)

templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ <h1>Chords-Python Applications</h1>
6767
<button type="submit" name="app_name" value="gui"
6868
class="{% if 'gui' in running_apps %}running{% else %}not-running{% endif %}"
6969
{% if not lsl_started %}disabled{% endif %}>
70-
GUI of 6 Channels
70+
GUI of Channels
7171
</button>
7272
<button type="submit" name="app_name" value="keystroke"
7373
class="{% if 'keystroke' in running_apps %}running{% else %}not-running{% endif %}"

0 commit comments

Comments
 (0)