From 9c7d6a7f06e36c208c9776a6ac0e756d5d27929f Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Sat, 10 May 2025 10:55:27 +0530 Subject: [PATCH 01/30] Connection timeout increase to 15 as wifi takes time to connect --- static/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/script.js b/static/script.js index 8ab3e95c..4d091707 100644 --- a/static/script.js +++ b/static/script.js @@ -463,7 +463,7 @@ connectBtn.addEventListener('click', async () => { // Poll connection status async function pollConnectionStatus() { let attempts = 0; - const maxAttempts = 5; // 5 seconds timeout + const maxAttempts = 15; // 15 seconds timeout const checkStatus = async () => { attempts++; From f44cbb76a7a6b3483fc7f13707ee000997179b8b Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Sat, 10 May 2025 10:56:38 +0530 Subject: [PATCH 02/30] Serial Connection Disconnection issue resolved --- connection.py | 63 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/connection.py b/connection.py index 590ad1d1..c1b848cc 100644 --- a/connection.py +++ b/connection.py @@ -29,6 +29,8 @@ def __init__(self): self.num_channels = 0 self.stream_active = False self.recording_active = False + self.usb_thread = None + self.usb_running = False async def get_ble_device(self): devices = await Chords_BLE.scan_devices() @@ -154,6 +156,25 @@ def notification_handler(sender, data): print(f"BLE connection failed: {str(e)}") return False + def usb_data_handler(self): + while self.usb_running: + try: + if self.usb_connection and hasattr(self.usb_connection, 'ser') and self.usb_connection.ser.is_open: + self.usb_connection.read_data() + + if hasattr(self.usb_connection, 'data'): + sample = self.usb_connection.data[:, -1] + if self.lsl_connection: + self.lsl_connection.push_sample(sample) + if self.recording_active: + self.log_to_csv(sample.tolist()) + time.sleep(0.001) # Small delay to prevent CPU overload + else: + time.sleep(0.1) + except Exception as e: + print(f"USB data handler error: {str(e)}") + break + def connect_usb(self): self.usb_connection = Chords_USB() if self.usb_connection.detect_hardware(): @@ -162,21 +183,16 @@ def connect_usb(self): self.setup_lsl(self.num_channels, sampling_rate) - original_read_data = self.usb_connection.read_data - def wrapped_read_data(): - original_read_data() - if hasattr(self.usb_connection, 'data') and self.lsl_connection: - sample = self.usb_connection.data[:, -1] - self.lsl_connection.push_sample(sample) - if self.recording_active: - self.log_to_csv(sample.tolist()) - - self.usb_connection.read_data = wrapped_read_data + # Start the USB streaming command + response = self.usb_connection.send_command('START') - # Start streaming in a separate thread - self.usb_thread = threading.Thread(target=self.usb_connection.start_streaming) + # Start the data handler thread + self.usb_running = True + self.usb_thread = threading.Thread(target=self.usb_data_handler) self.usb_thread.daemon = True self.usb_thread.start() + + print(f"USB connection established to {self.usb_connection.board}. Waiting for data...") return True return False @@ -213,7 +229,7 @@ def connect_wifi(self): self.log_to_csv(channel_data) except KeyboardInterrupt: - self.wifi_connection.disconnect() + self.wifi_connection.cleanup() print("\nDisconnected") finally: self.stop_csv_recording() @@ -227,10 +243,14 @@ def cleanup(self): if self.usb_connection: try: - self.usb_connection.cleanup() + self.usb_running = False # Signal the thread to stop + if self.usb_thread and self.usb_thread.is_alive(): + self.usb_thread.join(timeout=1) + + if hasattr(self.usb_connection, 'ser') and self.usb_connection.ser.is_open: + self.usb_connection.send_command('STOP') + self.usb_connection.ser.close() print("USB connection closed") - if hasattr(self, 'usb_thread') and self.usb_thread.is_alive(): - self.usb_thread.join(timeout=1) # Wait for thread to finish except Exception as e: print(f"Error closing USB connection: {str(e)}") finally: @@ -238,7 +258,7 @@ def cleanup(self): if self.ble_connection: try: - self.ble_connection.disconnect() + self.ble_connection.stop() print("BLE connection closed") except Exception as e: print(f"Error closing BLE connection: {str(e)}") @@ -247,7 +267,7 @@ def cleanup(self): if self.wifi_connection: try: - self.wifi_connection.disconnect() + self.wifi_connection.cleanup() print("WiFi connection closed") except Exception as e: print(f"Error closing WiFi connection: {str(e)}") @@ -270,7 +290,10 @@ def main(): try: if args.protocol == 'usb': - manager.connect_usb() + if manager.connect_usb(): + # Keep the main thread alive while USB is running + while manager.usb_running: + time.sleep(1) elif args.protocol == 'wifi': manager.connect_wifi() elif args.protocol == 'ble': @@ -279,6 +302,8 @@ def main(): print("\nCleanup Completed.") except Exception as e: print(f"Error: {str(e)}") + finally: + manager.cleanup() if __name__ == '__main__': main() \ No newline at end of file From 8a1730bba241ef7acb5b08ffad271f48b6d8b48c Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Sat, 10 May 2025 11:02:27 +0530 Subject: [PATCH 03/30] Serial Connection Disconnection issue resolved --- connection.py | 61 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/connection.py b/connection.py index 590ad1d1..0a2b53da 100644 --- a/connection.py +++ b/connection.py @@ -29,6 +29,8 @@ def __init__(self): self.num_channels = 0 self.stream_active = False self.recording_active = False + self.usb_thread = None + self.usb_running = False async def get_ble_device(self): devices = await Chords_BLE.scan_devices() @@ -154,6 +156,25 @@ def notification_handler(sender, data): print(f"BLE connection failed: {str(e)}") return False + def usb_data_handler(self): + while self.usb_running: + try: + if self.usb_connection and hasattr(self.usb_connection, 'ser') and self.usb_connection.ser.is_open: + self.usb_connection.read_data() + + if hasattr(self.usb_connection, 'data'): + sample = self.usb_connection.data[:, -1] + if self.lsl_connection: + self.lsl_connection.push_sample(sample) + if self.recording_active: + self.log_to_csv(sample.tolist()) + time.sleep(0.001) # Small delay to prevent CPU overload + else: + time.sleep(0.1) + except Exception as e: + print(f"USB data handler error: {str(e)}") + break + def connect_usb(self): self.usb_connection = Chords_USB() if self.usb_connection.detect_hardware(): @@ -162,19 +183,12 @@ def connect_usb(self): self.setup_lsl(self.num_channels, sampling_rate) - original_read_data = self.usb_connection.read_data - def wrapped_read_data(): - original_read_data() - if hasattr(self.usb_connection, 'data') and self.lsl_connection: - sample = self.usb_connection.data[:, -1] - self.lsl_connection.push_sample(sample) - if self.recording_active: - self.log_to_csv(sample.tolist()) - - self.usb_connection.read_data = wrapped_read_data + # Start the USB streaming command + self.usb_connection.send_command('START') - # Start streaming in a separate thread - self.usb_thread = threading.Thread(target=self.usb_connection.start_streaming) + # Start the data handler thread + self.usb_running = True + self.usb_thread = threading.Thread(target=self.usb_data_handler) self.usb_thread.daemon = True self.usb_thread.start() return True @@ -213,7 +227,7 @@ def connect_wifi(self): self.log_to_csv(channel_data) except KeyboardInterrupt: - self.wifi_connection.disconnect() + self.wifi_connection.cleanup() print("\nDisconnected") finally: self.stop_csv_recording() @@ -227,10 +241,14 @@ def cleanup(self): if self.usb_connection: try: - self.usb_connection.cleanup() + self.usb_running = False # Signal the thread to stop + if self.usb_thread and self.usb_thread.is_alive(): + self.usb_thread.join(timeout=1) + + if hasattr(self.usb_connection, 'ser') and self.usb_connection.ser.is_open: + self.usb_connection.send_command('STOP') + self.usb_connection.ser.close() print("USB connection closed") - if hasattr(self, 'usb_thread') and self.usb_thread.is_alive(): - self.usb_thread.join(timeout=1) # Wait for thread to finish except Exception as e: print(f"Error closing USB connection: {str(e)}") finally: @@ -238,7 +256,7 @@ def cleanup(self): if self.ble_connection: try: - self.ble_connection.disconnect() + self.ble_connection.stop() print("BLE connection closed") except Exception as e: print(f"Error closing BLE connection: {str(e)}") @@ -247,7 +265,7 @@ def cleanup(self): if self.wifi_connection: try: - self.wifi_connection.disconnect() + self.wifi_connection.cleanup() print("WiFi connection closed") except Exception as e: print(f"Error closing WiFi connection: {str(e)}") @@ -270,7 +288,10 @@ def main(): try: if args.protocol == 'usb': - manager.connect_usb() + if manager.connect_usb(): + # Keep the main thread alive while USB is running + while manager.usb_running: + time.sleep(1) elif args.protocol == 'wifi': manager.connect_wifi() elif args.protocol == 'ble': @@ -279,6 +300,8 @@ def main(): print("\nCleanup Completed.") except Exception as e: print(f"Error: {str(e)}") + finally: + manager.cleanup() if __name__ == '__main__': main() \ No newline at end of file From f2696937aa768ea96332304011ee70ebd45a8aa6 Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Sat, 10 May 2025 18:07:00 +0530 Subject: [PATCH 04/30] Updated UI for fft visualizer(Multi channels) --- ffteeg.py | 412 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 320 insertions(+), 92 deletions(-) diff --git a/ffteeg.py b/ffteeg.py index 501661b9..0df8546f 100644 --- a/ffteeg.py +++ b/ffteeg.py @@ -1,6 +1,6 @@ import numpy as np from collections import deque -from PyQt5.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QMainWindow, QWidget +from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QMainWindow, QWidget, QGridLayout, QScrollArea, QPushButton, QDialog, QCheckBox, QLabel, QComboBox, QFrame, QSizePolicy) from PyQt5.QtCore import Qt from pyqtgraph import PlotWidget import pyqtgraph as pg @@ -11,6 +11,51 @@ import math import time +class SettingBox(QDialog): + def __init__(self, num_channels, selected_eeg, selected_bp, parent=None): + super().__init__(parent) + self.setWindowTitle("Channel Selection Settings") + self.setGeometry(200, 200, 400, 400) + + self.layout = QVBoxLayout() + + # EEG Channel Selection + self.eeg_label = QLabel("Select EEG Channels to Display:") + self.layout.addWidget(self.eeg_label) + + self.eeg_checkboxes = [] + for i in range(num_channels): + cb = QCheckBox(f"Channel {i+1}") + cb.setChecked(i in selected_eeg) + self.eeg_checkboxes.append(cb) + self.layout.addWidget(cb) + + # Brainpower Channel Selection + self.bp_label = QLabel("\nSelect Brainpower Channel:") + self.layout.addWidget(self.bp_label) + + self.bp_combobox = QComboBox() + for i in range(num_channels): + self.bp_combobox.addItem(f"Channel {i+1}") + self.bp_combobox.setCurrentIndex(selected_bp) + self.layout.addWidget(self.bp_combobox) + + # OK Button + self.ok_button = QPushButton("OK") + self.ok_button.clicked.connect(self.validate_and_accept) + self.layout.addWidget(self.ok_button) + + self.setLayout(self.layout) + + def validate_and_accept(self): + # Ensure at least one EEG channel is selected + eeg_selected = any(cb.isChecked() for cb in self.eeg_checkboxes) + + if not eeg_selected: + self.eeg_checkboxes[0].setChecked(True) + + self.accept() + class EEGMonitor(QMainWindow): def __init__(self): super().__init__() @@ -20,50 +65,80 @@ def __init__(self): self.stream_active = True # Flag to check if the stream is active self.last_data_time = None # Variable to store the last data time + self.selected_eeg_channels = [] + self.selected_bp_channel = 0 - # Main layout split into two halves: top for EEG, bottom for FFT and Brainwaves + # Main layout self.central_widget = QWidget() - self.main_layout = QVBoxLayout(self.central_widget) - - # First half for EEG signal plot - self.eeg_plot_widget = PlotWidget(self) - self.eeg_plot_widget.setBackground('w') - self.eeg_plot_widget.showGrid(x=True, y=True) - self.eeg_plot_widget.setLabel('bottom', 'EEG Plot') - self.eeg_plot_widget.setYRange(-5000, 5000, padding=0) - self.eeg_plot_widget.setXRange(0, 4, padding=0) - self.eeg_plot_widget.setMouseEnabled(x=False, y=True) # Disable zoom - self.main_layout.addWidget(self.eeg_plot_widget) + self.main_layout = QHBoxLayout(self.central_widget) - # Second half for FFT and Brainwave Power, aligned horizontally - self.bottom_layout = QHBoxLayout() + # Left side: EEG plots with settings button + self.left_container = QWidget() + self.left_layout = QVBoxLayout(self.left_container) + + # Scroll area for EEG channels + self.eeg_scroll = QScrollArea() + self.eeg_scroll.setWidgetResizable(True) + self.eeg_container = QWidget() + self.eeg_layout = QVBoxLayout(self.eeg_container) + self.eeg_layout.setSpacing(0) # Remove spacing between plots + self.eeg_scroll.setWidget(self.eeg_container) + self.left_layout.addWidget(self.eeg_scroll) + + # Add a frame for the settings button in bottom right + self.settings_frame = QFrame() + self.settings_frame.setStyleSheet("QFrame { background-color: rgba(50, 50, 50, 150); border: 1px solid #888; border-radius: 5px; }") + self.settings_frame_layout = QHBoxLayout(self.settings_frame) + self.settings_frame_layout.setContentsMargins(5, 5, 5, 5) + + # Settings button with improved styling + self.settings_button = QPushButton("⚙️ Settings") + self.settings_button.setStyleSheet(""" + QPushButton { + background-color: #2c3e50; + color: #ecf0f1; + border: none; + border-radius: 3px; + padding: 4px 8px; + font-size: 16px; + } + QPushButton:hover { + background-color: #34495e; + } + """) - # FFT Plot (left side of the second half) - self.fft_plot = PlotWidget(self) - self.fft_plot.setBackground('w') - self.fft_plot.showGrid(x=True, y=True) - self.fft_plot.setLabel('bottom', 'FFT') - # self.fft_plot.setYRange(0, 500, padding=0) - self.fft_plot.setXRange(0, 50, padding=0) # Set x-axis to 0 to 50 Hz - # self.fft_plot.setMouseEnabled(x=False, y=False) # Disable zoom - self.fft_plot.setAutoVisible(y=True) # Allow y-axis to autoscale - self.bottom_layout.addWidget(self.fft_plot) + self.settings_button.clicked.connect(self.show_settings) + self.settings_frame_layout.addWidget(self.settings_button, alignment=Qt.AlignRight) + + self.left_layout.addWidget(self.settings_frame, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.left_container, stretch=1) - # Bar graph for brainwave power bands (right side of the second half) - self.bar_chart_widget = pg.PlotWidget(self) - self.bar_chart_widget.setBackground('w') + # Right side: FFT and Brainpower + self.right_container = QWidget() + self.right_layout = QVBoxLayout(self.right_container) + + # FFT Plot + self.fft_plot = PlotWidget() + self.fft_plot.setBackground('black') + self.fft_plot.showGrid(x=True, y=True, alpha=0.3) + self.fft_plot.setLabel('bottom', 'Frequency (Hz)') + self.fft_plot.setXRange(0, 50, padding=0) + self.fft_plot.setAutoVisible(y=True) + self.right_layout.addWidget(self.fft_plot, stretch=1) + + # Brainpower Plot + self.bar_chart_widget = pg.PlotWidget() + self.bar_chart_widget.setBackground('black') + self.bar_chart_widget.showGrid(x=True, y=True, alpha=0.3) self.bar_chart_widget.setLabel('bottom', 'Brainpower Bands') self.bar_chart_widget.setXRange(-0.5, 4.5) - self.bar_chart_widget.setMouseEnabled(x=False, y=False) # Disable zoom - # Add brainwave power bars - self.brainwave_bars = pg.BarGraphItem(x=[0, 1, 2, 3, 4], height=[0, 0, 0, 0, 0], width=0.5, brush='g') + self.bar_chart_widget.setMouseEnabled(x=False, y=False) + self.brainwave_bars = pg.BarGraphItem(x=[0, 1, 2, 3, 4], height=[0, 0, 0, 0, 0], width=0.5) self.bar_chart_widget.addItem(self.brainwave_bars) - # Set x-ticks for brainwave types self.bar_chart_widget.getAxis('bottom').setTicks([[(0, 'Delta'), (1, 'Theta'), (2, 'Alpha'), (3, 'Beta'), (4, 'Gamma')]]) - self.bottom_layout.addWidget(self.bar_chart_widget) - - # Add the bottom layout to the main layout - self.main_layout.addLayout(self.bottom_layout) + self.right_layout.addWidget(self.bar_chart_widget, stretch=1) + + self.main_layout.addWidget(self.right_container, stretch=1) self.setCentralWidget(self.central_widget) # Set up LSL stream inlet @@ -87,82 +162,235 @@ def __init__(self): print("Unable to connect to any LSL stream! Exiting...") sys.exit(0) - # Sampling rate - self.sampling_rate = int(self.inlet.info().nominal_srate()) + # Get stream info + self.stream_info = self.inlet.info() + self.sampling_rate = int(self.stream_info.nominal_srate()) + self.num_channels = self.stream_info.channel_count() print(f"Sampling rate: {self.sampling_rate} Hz") # Data and Buffers - self.eeg_data = deque(maxlen=500) # Initialize moving window with 500 samples - self.moving_window = deque(maxlen=500) # 500 samples for FFT and power calculation (sliding window) - + self.display_duration = 4 # seconds + self.buffer_size = self.display_duration * self.sampling_rate + self.eeg_data = [np.zeros(self.buffer_size) for _ in range(self.num_channels)] + self.current_indices = [0 for _ in range(self.num_channels)] + + # Moving window for FFT (separate from display buffer) + self.fft_window_size = 500 # samples for FFT calculation + self.moving_windows = [deque(maxlen=self.fft_window_size) for _ in range(self.num_channels)] + + # Initialize filters self.b_notch, self.a_notch = iirnotch(50, 30, self.sampling_rate) self.b_band, self.a_band = butter(4, [0.5 / (self.sampling_rate / 2), 48.0 / (self.sampling_rate / 2)], btype='band') + self.zi_notch = [lfilter_zi(self.b_notch, self.a_notch) * 0 for _ in range(self.num_channels)] + self.zi_band = [lfilter_zi(self.b_band, self.a_band) * 0 for _ in range(self.num_channels)] - self.zi_notch = lfilter_zi(self.b_notch, self.a_notch) * 0 - self.zi_band = lfilter_zi(self.b_band, self.a_band) * 0 - - # Timer for updating the plot + self.colors = ['#FF0054', '#00FF8C', '#00FF47', '#AA42FF','#FF8C19','#FF00FF','#00FFFF','#FFFF00'] + + # Initialize plots + self.eeg_plots = [] + self.eeg_curves = [] + self.fft_curves = [] + + self.selected_eeg_channels = list(range(self.num_channels)) # By default, select all channels + self.selected_bp_channel = 0 + + self.initialize_plots() + + # Timer for updating plots self.timer = pg.QtCore.QTimer() self.timer.timeout.connect(self.update_plot) self.timer.start(20) - - self.eeg_curve = self.eeg_plot_widget.plot(pen=pg.mkPen('b', width=1)) - self.fft_curve = self.fft_plot.plot(pen=pg.mkPen('r', width=1)) # FFT Colour is red - + + def initialize_plots(self): + for i in reversed(range(self.eeg_layout.count())): + widget = self.eeg_layout.itemAt(i).widget() + if widget is not None: + widget.setParent(None) + + self.eeg_plots = [] + self.eeg_curves = [] + + # Create EEG plots for all channels + for ch in range(self.num_channels): + plot = PlotWidget() + plot.setBackground('black') + plot.showGrid(x=True, y=True, alpha=0.3) + plot.setLabel('left', f'Ch {ch+1}', color='white') + plot.getAxis('left').setTextPen('white') + plot.getAxis('bottom').setTextPen('white') + plot.setYRange(-5000, 5000, padding=0) + plot.setXRange(0, self.display_duration, padding=0) + plot.setMouseEnabled(x=False, y=True) + plot.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + color = self.colors[ch % len(self.colors)] + pen = pg.mkPen(color=color, width=2) + curve = plot.plot(pen=pen) + + self.eeg_layout.addWidget(plot) + self.eeg_plots.append(plot) + self.eeg_curves.append((ch, curve)) + + plot.setVisible(False) # Initially hide all plots + + # Clear FFT plot and create curves for all channels + self.fft_plot.clear() + self.fft_curves = [] + + for ch in range(self.num_channels): + color = self.colors[ch % len(self.colors)] + pen = pg.mkPen(color=color, width=2) + self.fft_curves.append((ch, self.fft_plot.plot(pen=pen))) + + self.update_brainpower_colors() # Update brainpower bar colors + self.update_plot_visibility() # Now show only the selected channels + + def update_plot_visibility(self): + for plot in self.eeg_plots: + plot.setVisible(False) + + # Show only selected channels and set stretch factors + visible_count = len(self.selected_eeg_channels) + for idx, (ch, curve) in enumerate(self.eeg_curves): + if ch in self.selected_eeg_channels: + self.eeg_plots[idx].setVisible(True) + self.eeg_layout.setStretch(self.eeg_layout.indexOf(self.eeg_plots[idx]), 1) # Set stretch factor to distribute space equally + + # Update FFT curve visibility + for ch, curve in self.fft_curves: + curve.setVisible(ch in self.selected_eeg_channels) + + def update_brainpower_colors(self): + colors = [self.colors[0], self.colors[1], self.colors[2], self.colors[3], self.colors[4]] + self.brainwave_bars.setOpts(brushes=[pg.mkBrush(color) for color in colors]) + + def show_settings(self): + dialog = SettingBox(self.num_channels, self.selected_eeg_channels, self.selected_bp_channel, self) + if dialog.exec_(): + new_eeg_selection = [i for i, cb in enumerate(dialog.eeg_checkboxes) if cb.isChecked()] + new_bp_channel = dialog.bp_combobox.currentIndex() + + # Only update if selections actually changed + if (set(new_eeg_selection) != set(self.selected_eeg_channels) or (new_bp_channel != self.selected_bp_channel)): + self.selected_eeg_channels = new_eeg_selection + self.selected_bp_channel = new_bp_channel + self.update_plot_visibility() # Update plot visibility without recreating plots + + # Reset data buffers for the brainpower channel if it changed + if new_bp_channel != self.selected_bp_channel: + self.reset_brainpower_buffer() + + def reset_brainpower_buffer(self): + self.moving_windows[self.selected_bp_channel] = deque(maxlen=self.fft_window_size) + self.zi_notch[self.selected_bp_channel] = lfilter_zi(self.b_notch, self.a_notch) * 0 + self.zi_band[self.selected_bp_channel] = lfilter_zi(self.b_band, self.a_band) * 0 + def update_plot(self): - samples, _ = self.inlet.pull_chunk(timeout=0.0) + samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=50) if samples: - self.last_data_time = time.time() # Store the last data time + self.last_data_time = time.time() + for sample in samples: - raw_point = sample[0] - - notch_filtered, self.zi_notch = lfilter(self.b_notch, self.a_notch, [raw_point], zi=self.zi_notch) - band_filtered, self.zi_band = lfilter(self.b_band, self.a_band, notch_filtered, zi=self.zi_band) - band_filtered = band_filtered[-1] # Get the current filtered point - - # Update EEG data buffer - self.eeg_data.append(band_filtered) - - if len(self.moving_window) < 500: - self.moving_window.append(band_filtered) - else: - self.process_fft_and_brainpower() - - self.moving_window = deque(list(self.moving_window)[50:] + [band_filtered], maxlen=500) - - plot_data = np.array(self.eeg_data) - time_axis = np.linspace(0, 4, len(plot_data)) - self.eeg_curve.setData(time_axis, plot_data) - + for ch in range(self.num_channels): + raw_point = sample[ch] + + # Apply filters + notch_filtered, self.zi_notch[ch] = lfilter(self.b_notch, self.a_notch, [raw_point], zi=self.zi_notch[ch]) + band_filtered, self.zi_band[ch] = lfilter(self.b_band, self.a_band, notch_filtered, zi=self.zi_band[ch]) + band_filtered = band_filtered[-1] + + # Update EEG data buffer (circular buffer) + self.eeg_data[ch][self.current_indices[ch]] = band_filtered + self.current_indices[ch] = (self.current_indices[ch] + 1) % self.buffer_size + + # Update moving window for FFT + self.moving_windows[ch].append(band_filtered) + + # Update EEG plots for visible channels + time_axis = np.linspace(0, self.display_duration, self.buffer_size) + for ch, curve in self.eeg_curves: + if ch in self.selected_eeg_channels: + # Create properly ordered data from circular buffer + ordered_data = np.concatenate([ + self.eeg_data[ch][self.current_indices[ch]:], # From current index to end + self.eeg_data[ch][:self.current_indices[ch]] # From start to current index + ]) + curve.setData(time_axis, ordered_data) + + # Process FFT if we have enough data + if len(self.moving_windows[0]) == self.fft_window_size: + self.process_fft_and_brainpower() else: if self.last_data_time and (time.time() - self.last_data_time) > 2: self.stream_active = False print("LSL stream disconnected!") self.timer.stop() self.close() - + def process_fft_and_brainpower(self): - window = np.hanning(len(self.moving_window)) - buffer_windowed = np.array(self.moving_window) * window - fft_result = np.abs(np.fft.rfft(buffer_windowed)) - fft_result /= len(buffer_windowed) - freqs = np.fft.rfftfreq(len(buffer_windowed), 1 / self.sampling_rate) - self.fft_curve.setData(freqs, fft_result) - - brainwave_power = self.calculate_brainwave_power(fft_result, freqs) - self.brainwave_bars.setOpts(height=brainwave_power) - + # Calculate FFT for visible channels + all_fft_results = [] + freqs = None + + for ch, curve in self.fft_curves: + if ch not in self.selected_eeg_channels: + continue + if len(self.moving_windows[ch]) < 10: + continue + + window = np.hanning(len(self.moving_windows[ch])) + buffer_windowed = np.array(self.moving_windows[ch]) * window + fft_result = np.abs(np.fft.rfft(buffer_windowed)) + fft_result /= len(buffer_windowed) + + if freqs is None: + freqs = np.fft.rfftfreq(len(buffer_windowed), 1 / self.sampling_rate) + + min_len = min(len(freqs), len(fft_result)) + freqs = freqs[:min_len] + fft_result = fft_result[:min_len] + all_fft_results.append((ch, fft_result)) + + # Update FFT plots for visible channels + for ch, curve in self.fft_curves: + if ch in self.selected_eeg_channels: + fft_result = next((res for c, res in all_fft_results if c == ch), None) + if fft_result is not None: + curve.setData(freqs, fft_result) + + # Calculate brainpower for selected channel + if 0 <= self.selected_bp_channel < len(self.moving_windows): + ch = self.selected_bp_channel + if len(self.moving_windows[ch]) >= 10: + window = np.hanning(len(self.moving_windows[ch])) + buffer_windowed = np.array(self.moving_windows[ch]) * window + fft_result = np.abs(np.fft.rfft(buffer_windowed)) + fft_result /= len(buffer_windowed) + + if freqs is None: + freqs = np.fft.rfftfreq(len(buffer_windowed), 1 / self.sampling_rate) + + min_len = min(len(freqs), len(fft_result)) + freqs = freqs[:min_len] + fft_result = fft_result[:min_len] + + brainwave_power = self.calculate_brainwave_power(fft_result, freqs) + self.brainwave_bars.setOpts(height=brainwave_power) + def calculate_brainwave_power(self, fft_data, freqs): - delta_power = math.sqrt(np.sum(((fft_data[(freqs >= 0.5) & (freqs <= 4)])**2)/4)) - theta_power = math.sqrt(np.sum(((fft_data[(freqs >= 4) & (freqs <= 8)])**2)/5)) - alpha_power = math.sqrt(np.sum(((fft_data[(freqs >= 8) & (freqs <= 13)])**2)/6)) - beta_power = math.sqrt(np.sum(((fft_data[(freqs >= 13) & (freqs <=30)])**2)/18)) - gamma_power = math.sqrt(np.sum(((fft_data[(freqs >= 30) & (freqs <= 45)])**2)/16)) - print("Delta Power", delta_power) - print("Theta Power", theta_power) - print("Alpha Power", alpha_power) - print("Beta Power", beta_power) - print("Gamma Power", gamma_power) + delta_range = (freqs >= 0.5) & (freqs <= 4) + theta_range = (freqs >= 4) & (freqs <= 8) + alpha_range = (freqs >= 8) & (freqs <= 13) + beta_range = (freqs >= 13) & (freqs <= 30) + gamma_range = (freqs >= 30) & (freqs <= 45) + + delta_power = math.sqrt(np.sum(fft_data[delta_range]**2)/4) if np.any(delta_range) else 0 + theta_power = math.sqrt(np.sum(fft_data[theta_range]**2)/5) if np.any(theta_range) else 0 + alpha_power = math.sqrt(np.sum(fft_data[alpha_range]**2)/6) if np.any(alpha_range) else 0 + beta_power = math.sqrt(np.sum(fft_data[beta_range]**2)/18) if np.any(beta_range) else 0 + gamma_power = math.sqrt(np.sum(fft_data[gamma_range]**2)/16) if np.any(gamma_range) else 0 + + print(f"Brainpower (Ch {self.selected_bp_channel+1}): Delta: {delta_power:.2f} Theta: {theta_power:.2f} Alpha: {alpha_power:.2f} Beta: {beta_power:.2f} Gamma: {gamma_power:.2f}") return [delta_power, theta_power, alpha_power, beta_power, gamma_power] if __name__ == "__main__": From 2d703123a75e1d234febe8464ffb329935f02ffe Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 10:41:25 +0530 Subject: [PATCH 05/30] UI is done --- ffteeg.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ffteeg.py b/ffteeg.py index 0df8546f..2e995a21 100644 --- a/ffteeg.py +++ b/ffteeg.py @@ -272,13 +272,12 @@ def show_settings(self): # Only update if selections actually changed if (set(new_eeg_selection) != set(self.selected_eeg_channels) or (new_bp_channel != self.selected_bp_channel)): - self.selected_eeg_channels = new_eeg_selection - self.selected_bp_channel = new_bp_channel - self.update_plot_visibility() # Update plot visibility without recreating plots - - # Reset data buffers for the brainpower channel if it changed - if new_bp_channel != self.selected_bp_channel: - self.reset_brainpower_buffer() + bp_changed = new_bp_channel != self.selected_bp_channel + self.selected_eeg_channels = new_eeg_selection + self.selected_bp_channel = new_bp_channel + if bp_changed: + self.reset_brainpower_buffer() + self.update_plot_visibility() def reset_brainpower_buffer(self): self.moving_windows[self.selected_bp_channel] = deque(maxlen=self.fft_window_size) From 0388aaa96e874a5506a576443c7b3b0d5ee4541a Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 13:53:00 +0530 Subject: [PATCH 06/30] Remove not required things --- templates/index.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/templates/index.html b/templates/index.html index b65eb453..11e6139e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -134,13 +134,6 @@

Applications< From 97764898ddbbdf3f4bf7645bd64f17e5f7fb51a3 Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 14:00:10 +0530 Subject: [PATCH 07/30] Adding functionality- When an app is running then a green tick is shown on that app and that app is not clickable --- static/script.js | 136 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 38 deletions(-) diff --git a/static/script.js b/static/script.js index 4d091707..acd8b8a1 100644 --- a/static/script.js +++ b/static/script.js @@ -48,6 +48,7 @@ async function initializeApplication() { const apps = await loadApps(); renderApps(apps); setupCategoryFilter(apps); + startAppStatusChecker(); } catch (error) { console.error('Application initialization failed:', error); } @@ -71,7 +72,8 @@ function renderApps(apps) { apps.forEach(app => { const card = document.createElement('div'); - card.className = `group bg-gradient-to-b from-white to-gray-50 dark:from-gray-700 dark:to-gray-800 rounded-xl shadow border hover:shadow-lg transition-all duration-300 dark:border-gray-700 cursor-pointer overflow-hidden`; + card.className = `group bg-gradient-to-b from-white to-gray-50 dark:from-gray-700 dark:to-gray-800 rounded-xl shadow border hover:shadow-lg transition-all duration-300 dark:border-gray-700 overflow-hidden cursor-pointer`; + card.id = `card-${app.script}`; card.innerHTML = `
@@ -79,55 +81,112 @@ function renderApps(apps) {
-

${app.title}

+
+

${app.title}

+ +

${app.description}

`; + updateAppStatus(app.script); card.addEventListener('click', async () => { - if (!isConnected) { - showAlert('Please connect to a device first using USB, WiFi or Bluetooth'); - return; - } - - // Add loading state to the clicked card - const originalContent = card.innerHTML; - card.innerHTML = ` -
- - Launching ${app.title}... -
- `; - - try { - const response = await fetch(`/check_app_status/${app.script}`); - - if (!response.ok) { - throw new Error('Failed to check app status'); - } - - const data = await response.json(); - - if (data.status === 'running') { - showAlert(`${app.title} is already running!`); - card.innerHTML = originalContent; - return; - } - - await launchApplication(app.script); - card.innerHTML = originalContent; - } catch (error) { - console.error('Error launching app:', error); - showAlert(`Failed to launch ${app.title}: ${error.message}`); - card.innerHTML = originalContent; - } + await handleAppClick(app, card); }); appGrid.appendChild(card); }); } +async function handleAppClick(app, card) { + const statusElement = document.getElementById(`status-${app.script}`); + if (statusElement && !statusElement.classList.contains('hidden')) { + return; + } + + if (!isConnected) { + showAlert('Please connect to a device first using USB, WiFi or Bluetooth'); + return; + } + + const originalContent = card.innerHTML; // Add loading state to the clicked card + card.innerHTML = ` +
+ + Launching ${app.title}... +
+ `; + + try { + const response = await fetch(`/check_app_status/${app.script}`); + + if (!response.ok) { + throw new Error('Failed to check app status'); + } + + const data = await response.json(); + + await launchApplication(app.script); + card.innerHTML = originalContent; + updateAppStatus(app.script); // Update status after launch + } catch (error) { + console.error('Error launching app:', error); + showAlert(`Failed to launch ${app.title}: ${error.message}`); + card.innerHTML = originalContent; + } +} + +async function updateAppStatus(appName) { + try { + const response = await fetch(`/check_app_status/${appName}`); + if (!response.ok) return; + + const data = await response.json(); + const statusElement = document.getElementById(`status-${appName}`); + const cardElement = document.getElementById(`card-${appName}`); + + if (statusElement && cardElement) { + if (data.status === 'running') { + statusElement.classList.remove('hidden'); + cardElement.classList.add('cursor-not-allowed'); + cardElement.classList.remove('cursor-pointer'); + cardElement.classList.remove('hover:shadow-lg'); + cardElement.classList.add('opacity-60'); + } else { + statusElement.classList.add('hidden'); + cardElement.style.pointerEvents = 'auto'; + cardElement.classList.remove('cursor-not-allowed'); + cardElement.classList.add('cursor-pointer'); + cardElement.classList.add('hover:shadow-lg'); + cardElement.classList.remove('opacity-80'); + } + } + } catch (error) { + console.error('Error checking app status:', error); + } +} + +// Periodically check all app statuses +function startAppStatusChecker() { + checkAllAppStatuses(); + setInterval(checkAllAppStatuses, 200); +} + +// Check status of all apps +function checkAllAppStatuses() { + const appGrid = document.getElementById('app-grid'); + if (!appGrid) return; + + const apps = appGrid.querySelectorAll('[id^="status-"]'); + apps.forEach(statusElement => { + const appName = statusElement.id.replace('status-', ''); + updateAppStatus(appName); + }); +} + // Set up category filter with fixed options function setupCategoryFilter(apps) { const categorySelect = document.querySelector('select'); @@ -179,6 +238,7 @@ function filterAppsByCategory(category, allApps) { appGrid.style.opacity = '0'; setTimeout(() => { appGrid.style.opacity = '1'; + checkAllAppStatuses(); }, 10); }, 300); } From 00ba008e73dbc3d1289eed534f103db5cf4a15eb Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 15:38:46 +0530 Subject: [PATCH 08/30] Opacity reduces when an app is running --- static/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/script.js b/static/script.js index acd8b8a1..fbfc4da7 100644 --- a/static/script.js +++ b/static/script.js @@ -161,7 +161,7 @@ async function updateAppStatus(appName) { cardElement.classList.remove('cursor-not-allowed'); cardElement.classList.add('cursor-pointer'); cardElement.classList.add('hover:shadow-lg'); - cardElement.classList.remove('opacity-80'); + cardElement.classList.remove('opacity-60'); } } } catch (error) { From ebbba0e3065693fb75fa6f5a63c4f4262db1c789 Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 15:44:04 +0530 Subject: [PATCH 09/30] CSV Filename can't be editable when recording is On. --- static/script.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/static/script.js b/static/script.js index fbfc4da7..632e1e67 100644 --- a/static/script.js +++ b/static/script.js @@ -332,6 +332,9 @@ function initializeFilename() { const defaultName = `ChordsPy_${getTimestamp()}`; filenameInput.value = defaultName; filenameInput.placeholder = defaultName; + filenameInput.disabled = false; // Ensure input is enabled initially + filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); + filenameInput.classList.add('dark:bg-gray-800'); } // Sanitize filename input - replace spaces and dots with underscores @@ -722,6 +725,11 @@ function toggleRecording() { recordBtn.classList.remove('bg-gray-500'); recordBtn.classList.add('bg-red-500', 'hover:bg-red-600'); recordingStatus.classList.add('hidden'); + + // Enable filename input + filenameInput.disabled = false; + filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); + filenameInput.classList.add('dark:bg-gray-800'); } }) .catch(error => { @@ -742,6 +750,11 @@ function toggleRecording() { recordBtn.classList.remove('bg-red-500', 'hover:bg-red-600'); recordBtn.classList.add('bg-gray-500'); recordingStatus.classList.remove('hidden'); + + // Disable filename input + filenameInput.disabled = true; + filenameInput.classList.add('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); + filenameInput.classList.remove('dark:bg-gray-800'); } }) .catch(error => { @@ -803,6 +816,11 @@ function checkStreamStatus() { recordBtn.classList.remove('bg-gray-500'); recordBtn.classList.add('bg-red-500', 'hover:bg-red-600'); recordingStatus.classList.add('hidden'); + + // Enable filename input if recording was stopped due to disconnection + filenameInput.disabled = false; + filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); + filenameInput.classList.add('dark:bg-gray-800'); } // Stop console updates From cde94b331d25d6c431bed6e4eb8d2dca19a36601 Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 15:58:10 +0530 Subject: [PATCH 10/30] Toast for Recording started, Recording stopped added(also timeout increase to 3 sec) --- static/script.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/static/script.js b/static/script.js index 632e1e67..8bf8d814 100644 --- a/static/script.js +++ b/static/script.js @@ -492,8 +492,6 @@ connectBtn.addEventListener('click', async () => { postData.device_address = selectedBleDevice.address; } - // showStatus('Connecting...', 'fa-spinner fa-spin', 'text-blue-500'); - const response = await fetch('/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -621,7 +619,6 @@ disconnectBtn.addEventListener('click', async () => { // Show connecting state during disconnection disconnectBtn.classList.add('hidden'); disconnectingBtn.classList.remove('hidden'); - // showStatus('Disconnecting...', 'fa-spinner fa-spin', 'text-blue-500'); const response = await fetch('/disconnect', { method: 'POST' }); const data = await response.json(); @@ -631,6 +628,7 @@ disconnectBtn.addEventListener('click', async () => { // Return to connect state disconnectingBtn.classList.add('hidden'); connectBtn.classList.remove('hidden'); + showStatus('Disconnected!', 'fa-times-circle', 'text-red-500'); // Reset all protocol buttons connectionBtns.forEach(btn => { @@ -730,6 +728,7 @@ function toggleRecording() { filenameInput.disabled = false; filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); filenameInput.classList.add('dark:bg-gray-800'); + showStatus('Recording stopped', 'fa-stop-circle', 'text-red-500'); } }) .catch(error => { @@ -755,6 +754,7 @@ function toggleRecording() { filenameInput.disabled = true; filenameInput.classList.add('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); filenameInput.classList.remove('dark:bg-gray-800'); + showStatus('Recording started', 'fa-record-vinyl', 'text-green-500'); } }) .catch(error => { @@ -775,7 +775,7 @@ function showStatus(text, icon, colorClass) { statusDiv.classList.remove('hidden'); setTimeout(() => { statusDiv.classList.add('hidden'); - }, 2000); + }, 3000); } function showAlert(message) { @@ -805,6 +805,7 @@ function checkStreamStatus() { disconnectingBtn.classList.add('hidden'); connectingBtn.classList.add('hidden'); connectBtn.classList.remove('hidden'); + showStatus('Disconnected!', 'fa-times-circle', 'text-red-500'); // Re-enable protocol buttons setProtocolButtonsDisabled(false); @@ -821,6 +822,7 @@ function checkStreamStatus() { filenameInput.disabled = false; filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); filenameInput.classList.add('dark:bg-gray-800'); + showStatus('Recording stopped (connection lost)', 'fa-stop-circle', 'text-red-500'); } // Stop console updates From 409ea22c3c2981bef5e8afcc2fd356fcf75bd622 Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 16:25:33 +0530 Subject: [PATCH 11/30] CSV Filename timer update regularly --- static/script.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/static/script.js b/static/script.js index 8bf8d814..757f4af1 100644 --- a/static/script.js +++ b/static/script.js @@ -290,6 +290,26 @@ let isRecording = false; let eventSource = null; let isScanning = false; +// Function to update the filename timestamp periodically +function startTimestampUpdater() { + updateFilenameTimestamp(); + setInterval(updateFilenameTimestamp, 1000); +} + +// Update the filename timestamp in the input field +function updateFilenameTimestamp() { + // Only update if recording is stop + if (!isRecording) { + const defaultName = `ChordsPy_${getTimestamp()}`; + filenameInput.placeholder = defaultName; + + // If the input is empty or has the default pattern, update the value too + if (!filenameInput.value || filenameInput.value.startsWith('ChordsPy_')) { + filenameInput.value = defaultName; + } + } +} + // Function to generate timestamp for filename function getTimestamp() { const now = new Date(); @@ -335,6 +355,7 @@ function initializeFilename() { filenameInput.disabled = false; // Ensure input is enabled initially filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); filenameInput.classList.add('dark:bg-gray-800'); + startTimestampUpdater(); } // Sanitize filename input - replace spaces and dots with underscores @@ -728,6 +749,7 @@ function toggleRecording() { filenameInput.disabled = false; filenameInput.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'cursor-not-allowed'); filenameInput.classList.add('dark:bg-gray-800'); + updateFilenameTimestamp() showStatus('Recording stopped', 'fa-stop-circle', 'text-red-500'); } }) From 44fb6551a72fccad6f1059a7b18adaad9f8bdaaa Mon Sep 17 00:00:00 2001 From: Payal Lakra Date: Mon, 12 May 2025 17:49:24 +0530 Subject: [PATCH 12/30] Disconnect toast added with colour green(white text) --- app.py | 5 ++--- connection.py | 1 + static/script.js | 38 ++++++++++++++++++++++++++++++++++++-- templates/index.html | 4 ++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index eff7cf8f..3d569a67 100644 --- a/app.py +++ b/app.py @@ -63,9 +63,8 @@ async def scan_ble_devices(): @app.route('/check_stream') def check_stream(): - if connection_manager and connection_manager.stream_active: - return jsonify({'connected': True}) - return jsonify({'connected': False}) + is_connected = connection_manager.stream_active if hasattr(connection_manager, 'stream_active') else False + return jsonify({'connected': is_connected}) @app.route('/check_connection') def check_connection(): diff --git a/connection.py b/connection.py index 0a2b53da..f3b0510d 100644 --- a/connection.py +++ b/connection.py @@ -237,6 +237,7 @@ def cleanup(self): if self.lsl_connection: self.lsl_connection = None + self.stream_active = False print("LSL stream stopped") if self.usb_connection: diff --git a/static/script.js b/static/script.js index 757f4af1..dd4316ee 100644 --- a/static/script.js +++ b/static/script.js @@ -608,7 +608,7 @@ function handleConnectionSuccess() { btn.disabled = true; }); - showStatus(`Connected via ${selectedProtocol.toUpperCase()}`, 'fa-check-circle', 'text-green-500'); + showStatus(`Connected via ${selectedProtocol.toUpperCase()}`, 'fa-check-circle'); // Start console updates startConsoleUpdates(); @@ -793,7 +793,7 @@ initializeFilename(); // Set default filename with timestamp function showStatus(text, icon, colorClass) { const statusDiv = document.getElementById('connection-status'); statusText.textContent = text; - statusIcon.innerHTML = ``; + statusIcon.innerHTML = ``; statusDiv.classList.remove('hidden'); setTimeout(() => { statusDiv.classList.add('hidden'); @@ -811,6 +811,7 @@ function checkStreamStatus() { if (data.connected) { // If connected, update the frontend if (!isConnected) { + handleConnectionSuccess(); isConnected = true; connectBtn.classList.add('hidden'); connectingBtn.classList.add('hidden'); @@ -822,6 +823,7 @@ function checkStreamStatus() { } else { // If not connected, update the frontend if (isConnected) { + handleDisconnection(); isConnected = false; disconnectBtn.classList.add('hidden'); disconnectingBtn.classList.add('hidden'); @@ -860,6 +862,38 @@ function checkStreamStatus() { }); } +function handleDisconnection() { + isConnected = false; + disconnectBtn.classList.add('hidden'); + disconnectingBtn.classList.add('hidden'); + connectingBtn.classList.add('hidden'); + connectBtn.classList.remove('hidden'); + showStatus('Stream disconnected!', 'fa-times-circle', 'text-red-500'); + + // Reset protocol buttons + connectionBtns.forEach(btn => { + btn.disabled = false; + btn.classList.remove('bg-cyan-600', 'dark:bg-cyan-700', 'cursor-default'); + btn.classList.add('hover:bg-cyan-500', 'hover:text-white'); + }); + + // Handle recording state + if (isRecording) { + isRecording = false; + recordBtn.innerHTML = 'Start Recording'; + recordBtn.classList.remove('bg-gray-500'); + recordBtn.classList.add('bg-red-500', 'hover:bg-red-600'); + recordingStatus.classList.add('hidden'); + filenameInput.disabled = false; + showStatus('Recording stopped (stream lost)', 'fa-stop-circle', 'text-red-500'); + } + + if (eventSource) { + eventSource.close(); + eventSource = null; + } +} + // Call the checkStreamStatus function every 1 second setInterval(checkStreamStatus, 1000); diff --git a/templates/index.html b/templates/index.html index 11e6139e..eaf842a1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -74,9 +74,9 @@ -