Skip to content

Commit 0b503e6

Browse files
authored
Merge pull request #39 from PayalLakra/bio_amptool
Add code comments for better readability
2 parents 08711f5 + 651de9c commit 0b503e6

File tree

11 files changed

+299
-341
lines changed

11 files changed

+299
-341
lines changed

app.py

Lines changed: 85 additions & 68 deletions
Large diffs are not rendered by default.

chords.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,8 @@ def main():
357357
else:
358358
ser = detect_hardware(baudrate=args.baudrate)
359359
if ser is None:
360-
sys.stderr.write("No\n")
361-
sys.exit(1) # Exit with a non-zero code to indicate failure
360+
sys.stderr.write("Serial Connection not established properly.Try Again\n")
361+
sys.exit(1) # Exit with a non-zero code to indicate failure
362362
if ser is None:
363363
print("Arduino port not specified or detected. Exiting.") # Notify if no port is available
364364
return

chords_requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
numpy==2.1.3
22
pylsl==1.16.2
3-
pyserial==3.5
3+
pyserial==3.5
4+
bleak==0.22.3

csvplotter.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import plotly.graph_objects as go
55

66
class CSVPlotterApp:
7-
def __init__(self, root):
7+
def __init__(self, root): # Initialize the main application window
88
self.root = root
99
self.root.title("CSV Plotter GUI")
10-
self.filename = None
11-
self.data = None
12-
self.create_widgets()
10+
self.filename = None # Variable to store the selected CSV file name
11+
self.data = None # Variable to store the loaded data
12+
self.create_widgets() # Call the method to create widgets
1313

1414
def create_widgets(self):
1515
# Create a frame for buttons and file name display
@@ -41,19 +41,19 @@ def create_widgets(self):
4141
self.plot_button.pack(pady=10)
4242

4343
def load_csv(self):
44-
self.filename = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
44+
self.filename = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")]) # Open file dialog to select CSV file
4545
if self.filename:
4646
try:
47-
with open(self.filename, "r", encoding="utf-8") as f:
48-
lines = f.readlines()
47+
with open(self.filename, "r", encoding="utf-8") as f: # Open the selected CSV file
48+
lines = f.readlines() # Read all lines into a list for header detection
4949

50-
header_index = None # Find the row where 'Counter' appears
51-
for i, line in enumerate(lines):
50+
header_index = None # Initialize the variable to track where (Header)'Counter' appears
51+
for i, line in enumerate(lines): # Iterate through lines to find the header line
5252
if "Counter" in line:
53-
header_index = i
54-
break
53+
header_index = i # Store the index of the line containing 'Counter'
54+
break # If found, break the loop
5555

56-
if header_index is None:
56+
if header_index is None: # If no header row with 'Counter' was found, show error and exit
5757
messagebox.showerror("Error", "CSV file must contain a 'Counter' column.")
5858
return
5959

@@ -73,7 +73,7 @@ def load_csv(self):
7373
messagebox.showerror("Error", f"Could not load CSV file: {e}")
7474

7575
def setup_dropdown_menu(self):
76-
# Get available channel columns (Channel1 to Channel6)
76+
# Get available channel columns
7777
channel_columns = [col for col in self.data.columns if 'Channel' in col]
7878

7979
# Populate dropdown menu with available channels
@@ -82,6 +82,7 @@ def setup_dropdown_menu(self):
8282
self.channel_selection.set(channel_columns[0]) # Default selection to the first channel
8383

8484
def plot_data(self):
85+
"""Creates an interactive plot of the selected data channel using Plotly."""
8586
selected_channel = self.channel_selection.get() # Get the selected channel
8687
if not selected_channel:
8788
messagebox.showerror("Error", "No channel selected for plotting")
@@ -96,9 +97,9 @@ def plot_data(self):
9697
yaxis_title="Value",
9798
template="plotly_white"
9899
)
99-
fig.show()
100+
fig.show() # Display the plot in a new window
100101

101102
if __name__ == "__main__":
102-
root = tk.Tk()
103-
app = CSVPlotterApp(root)
104-
root.mainloop()
103+
root = tk.Tk() # Create the main Tkinter root window
104+
app = CSVPlotterApp(root) # Create an instance of the CSVPlotterApp class
105+
root.mainloop() # Start the Tkinter main loop

gui.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def plot_lsl_data():
3030
global inlet, num_channels, data
3131

3232
print("Searching for available LSL streams...")
33-
streams = resolve_streams()
34-
available_streams = [s.name() for s in streams]
33+
streams = resolve_streams() # Discover available LSL streams
34+
available_streams = [s.name() for s in streams] # Get list of stream names
3535

3636
if not available_streams:
3737
print("No LSL streams found!")
@@ -43,7 +43,7 @@ def plot_lsl_data():
4343

4444
if resolved_streams:
4545
print(f"Successfully connected to {stream_name}!")
46-
inlet = StreamInlet(resolved_streams[0])
46+
inlet = StreamInlet(resolved_streams[0]) # Create an inlet to receive data from the stream
4747
break
4848
else:
4949
print(f"Failed to connect to {stream_name}.")

heartbeat_ecg.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ def __init__(self):
1515
self.setWindowTitle("Real-Time ECG Monitor") # Set up GUI window
1616
self.setGeometry(100, 100, 800, 600)
1717

18-
self.plot_widget = PlotWidget(self)
19-
self.plot_widget.setBackground('w')
20-
self.plot_widget.showGrid(x=True, y=True)
18+
self.plot_widget = PlotWidget(self) # Create the plotting widget
19+
self.plot_widget.setBackground('w') # Set background color to white
20+
self.plot_widget.showGrid(x=True, y=True) # Show grid lines
2121

2222
# Heart rate label at the bottom
2323
self.heart_rate_label = QLabel(self)
2424
self.heart_rate_label.setStyleSheet("font-size: 20px; font-weight: bold; color: black;")
2525
self.heart_rate_label.setAlignment(Qt.AlignCenter)
2626

27+
# Layout setup - vertical layout for plot and label
2728
layout = QVBoxLayout()
2829
layout.addWidget(self.plot_widget)
2930
layout.addWidget(self.heart_rate_label)
3031

32+
# Set the central widget that holds all other widgets
3133
central_widget = QWidget()
3234
central_widget.setLayout(layout)
3335
self.setCentralWidget(central_widget)
@@ -36,6 +38,7 @@ def __init__(self):
3638
print("Searching for available LSL streams...")
3739
available_streams = pylsl.resolve_streams()
3840

41+
# Exit if no streams are found
3942
if not available_streams:
4043
print("No LSL streams found! Exiting...")
4144
sys.exit(0)
@@ -53,21 +56,21 @@ def __init__(self):
5356
print("Unable to connect to any LSL stream! Exiting...")
5457
sys.exit(0)
5558

56-
# Sampling rate
59+
# Get Sampling rate from the stream info.
5760
self.sampling_rate = int(self.inlet.info().nominal_srate())
5861
print(f"Sampling rate: {self.sampling_rate} Hz")
5962

6063
# Data and buffers
6164
self.buffer_size = self.sampling_rate * 10 # Fixed-size buffer for 10 seconds
6265
self.ecg_data = np.zeros(self.buffer_size) # Fixed-size array for circular buffer
6366
self.time_data = np.linspace(0, 10, self.buffer_size) # Fixed time array for plotting
64-
self.r_peaks = [] # Store the indices of R-peaks
65-
self.heart_rate = None
67+
self.r_peaks = [] # Store the indices of R-peaks
68+
self.heart_rate = None # Initialize heart rate variable
6669
self.current_index = 0 # Index for overwriting data
6770

6871
self.b, self.a = butter(4, 20.0 / (0.5 * self.sampling_rate), btype='low') # Low-pass filter coefficients
6972

70-
self.timer = pg.QtCore.QTimer() # Timer for updating the plot
73+
self.timer = pg.QtCore.QTimer() # Timer for updating the plot (every 10 ms)
7174
self.timer.timeout.connect(self.update_plot)
7275
self.timer.start(10)
7376

@@ -86,7 +89,7 @@ def __init__(self):
8689
self.moving_average_window_size = 5 # Initialize moving average buffer
8790
self.heart_rate_history = [] # Buffer to store heart rates for moving average
8891

89-
# Connect double-click event
92+
# Connect double-click event for zoom reset
9093
self.plot_widget.scene().sigMouseClicked.connect(self.on_double_click)
9194

9295
def on_double_click(self, event):
@@ -139,11 +142,11 @@ def calculate_heart_rate(self):
139142
# Update heart rate label with moving average & convert into int
140143
self.heart_rate_label.setText(f"Heart Rate: {int(moving_average_hr)} BPM")
141144
else:
142-
self.heart_rate_label.setText("Heart Rate: Calculating...")
145+
self.heart_rate_label.setText("Heart Rate: Calculating...") # Display message if not enough R-peaks detected
143146

144147
def plot_r_peaks(self, filtered_ecg):
145-
r_peak_times = self.time_data[self.r_peaks] # Extract the time of detected R-peaks
146-
r_peak_values = filtered_ecg[self.r_peaks]
148+
r_peak_times = self.time_data[self.r_peaks] # Extract the time of detected R-peaks
149+
r_peak_values = filtered_ecg[self.r_peaks] # Get corresponding ECG values
147150
self.r_peak_curve.setData(r_peak_times, r_peak_values) # Plot R-peaks as red dots
148151

149152
if __name__ == "__main__":

keystroke.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@
99

1010
class EOGPeakDetector:
1111
def __init__(self, blink_button, keystroke_action, connect_button):
12-
self.inlet = None
13-
self.sampling_rate = None
14-
self.buffer_size = None
15-
self.eog_data = None
16-
self.current_index = 0
17-
self.b, self.a = None, None
18-
self.blink_button = blink_button
19-
self.keystroke_action = keystroke_action
20-
self.connect_button = connect_button
21-
self.blink_detected = False
22-
self.running = False
23-
self.connected = False
24-
self.last_blink_time = 0
25-
self.refractory_period = 0.2
26-
self.stop_threads = False
12+
self.inlet = None # LSL inlet for receiving data
13+
self.sampling_rate = None # Sampling rate of the data stream
14+
self.buffer_size = None # Size of the buffer for storing EOG data
15+
self.eog_data = None # Buffer for EOG data
16+
self.current_index = 0 # Current index in the buffer
17+
self.b, self.a = None, None # Filter coefficients for low-pass filter
18+
self.blink_button = blink_button # Button to trigger blink action
19+
self.keystroke_action = keystroke_action # Action to perform on blink detection
20+
self.connect_button = connect_button # Button to connect to LSL stream
21+
self.blink_detected = False # Flag to indicate if a blink has been detected
22+
self.running = False # Flag to control the detection loop
23+
self.connected = False # Flag to indicate if connected to LSL stream
24+
self.last_blink_time = 0 # Last time a blink was detected
25+
self.refractory_period = 0.2 # Refractory period to prevent multiple eye blink detections
26+
self.stop_threads = False # Flag to stop threads
2727

2828
def initialize_stream(self):
29+
"""Initialize the LSL stream connection and set up the buffer and filter coefficients."""
2930
print("Searching for available LSL streams...")
3031
available_streams = pylsl.resolve_streams()
3132

@@ -42,10 +43,10 @@ def initialize_stream(self):
4243
print(f"Sampling rate: {self.sampling_rate} Hz")
4344

4445
# Set buffer size and filter coefficients
45-
self.buffer_size = self.sampling_rate * 1
46-
self.eog_data = np.zeros(self.buffer_size)
47-
self.b, self.a = butter(4, 10.0 / (0.5 * self.sampling_rate), btype='low')
48-
self.connected = True
46+
self.buffer_size = self.sampling_rate * 1 # Buffer size for 1 second of data
47+
self.eog_data = np.zeros(self.buffer_size) # Initialize buffer for EOG data
48+
self.b, self.a = butter(4, 10.0 / (0.5 * self.sampling_rate), btype='low') # Low-pass filter coefficients
49+
self.connected = True # Set connected flag to True(LSL Stream connected)
4950
print("LSL stream connected successfully.")
5051
return True # Stop trying after first successful connection
5152

@@ -57,27 +58,31 @@ def initialize_stream(self):
5758
return False
5859

5960
def start_detection(self):
61+
"""Start the peak detection process"""
6062
print("Starting peak detection...")
61-
self.running = True
63+
self.running = True # Flag to control the detection loop
6264
while self.running:
6365
try:
6466
samples, _ = self.inlet.pull_chunk(timeout=1.0, max_samples=1)
6567
if samples:
6668
for sample in samples:
67-
self.eog_data[self.current_index] = sample[0]
68-
self.current_index = (self.current_index + 1) % self.buffer_size
69+
self.eog_data[self.current_index] = sample[0] # Store sample in circular buffer at current position
70+
self.current_index = (self.current_index + 1) % self.buffer_size # Update index with wrap-around using modulo
6971

7072
filtered_eog = lfilter(self.b, self.a, self.eog_data)
71-
self.detect_blinks(filtered_eog)
73+
self.detect_blinks(filtered_eog) # Run blink detection on the filtered signal
7274
except Exception as e:
7375
print(f"Error in detection: {e}")
7476
break
7577

7678
def stop_detection(self):
79+
"""Stop the peak detection process"""
7780
print("Stopping peak detection...")
78-
self.running = False
81+
self.running = False # Set running flag to False to stop the detection loop
7982

8083
def detect_blinks(self, filtered_eog):
84+
"""Detect blinks in the filtered EOG signal using a threshold-based method."""
85+
# Calculate dynamic threshold based on signal statistics
8186
mean_signal = np.mean(filtered_eog)
8287
stdev_signal = np.std(filtered_eog)
8388
threshold = mean_signal + (1.7 * stdev_signal)
@@ -87,8 +92,8 @@ def detect_blinks(self, filtered_eog):
8792
if start_index < 0:
8893
start_index = 0
8994

90-
filtered_window = filtered_eog[start_index:self.current_index]
91-
peaks = self.detect_peaks(filtered_window, threshold)
95+
filtered_window = filtered_eog[start_index:self.current_index] # Get the current window of filtered EOG data
96+
peaks = self.detect_peaks(filtered_window, threshold) # Detect peaks above threshold in the current window
9297

9398
current_time = time.time()
9499
if peaks and (current_time - self.last_blink_time > self.refractory_period):
@@ -105,6 +110,7 @@ def detect_peaks(self, signal, threshold):
105110
return peaks
106111

107112
def trigger_action(self):
113+
"""Trigger the keystroke action when a blink is detected."""
108114
if not self.blink_detected:
109115
self.blink_detected = True
110116
print("Triggering action...")
@@ -121,17 +127,20 @@ def update_button_color(self):
121127
self.blink_button.after(100, lambda: self.blink_button.config(bg="SystemButtonFace"))
122128

123129
def quit_action(detector):
130+
"""Handle the quit action for the GUI."""
124131
print("Quit button pressed. Exiting program.")
125132
detector.stop_threads = True
126133
detector.stop_detection()
127134
popup.quit()
128135
popup.destroy()
129136

130137
def keystroke_action():
138+
"""Perform the keystroke action (press spacebar)."""
131139
print("Spacebar pressed!")
132140
pyautogui.press('space')
133141

134142
def connect_start_stop_action(detector, connect_button):
143+
"""Handle the connect/start/stop action for the GUI."""
135144
if not detector.connected:
136145
print("Connect button pressed. Starting connection in a new thread.")
137146
threading.Thread(target=connect_to_stream, args=(detector, connect_button), daemon=True).start()
@@ -152,6 +161,7 @@ def connect_to_stream(detector, connect_button):
152161
print("Failed to connect to LSL stream.")
153162

154163
def create_popup():
164+
"""Create the popup window for the EOG keystroke emulator."""
155165
global popup
156166
popup = tk.Tk()
157167
popup.geometry("300x120")
@@ -196,8 +206,8 @@ def move(event):
196206

197207
detector = EOGPeakDetector(blink_button, keystroke_action, connect_button)
198208

199-
connect_button.config(command=lambda: connect_start_stop_action(detector, connect_button))
200-
quit_button.config(command=lambda: quit_action(detector))
209+
connect_button.config(command=lambda: connect_start_stop_action(detector, connect_button)) # Connect/Start/Stop action
210+
quit_button.config(command=lambda: quit_action(detector)) # Quit action
201211

202212
popup.mainloop()
203213

0 commit comments

Comments
 (0)