Skip to content

Changes in UI and 3 different protocols-USB, WIFI, BLE added #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

PayalLakra
Copy link
Member

@PayalLakra PayalLakra commented May 2, 2025

Summary by CodeRabbit

  • New Features

    • Unified connection interface for USB, WiFi, and BLE devices with protocol-specific buttons and BLE device selection popup.
    • Added disconnect button and CSV logging toggle to the UI.
    • Support for launching and stopping applications directly from the interface, with app status polling and button state management.
    • Enhanced real-time detection and graceful handling of device or stream disconnections across all monitoring and plotting tools.
  • Improvements

    • Redesigned and modernized UI with improved layout, styling, and responsiveness.
    • Streamlined connection and application management with clear status updates and error handling.
    • Client-driven asynchronous operations replace server-side forms and legacy SSE updates.
  • Bug Fixes

    • Improved detection and handling of lost or unavailable data streams, ensuring clean shutdowns and user notifications.
  • Refactor

    • Major backend and frontend codebase restructuring for modularity, maintainability, and unified user experience.
    • Removal of legacy scripts and session-based device management; introduction of modular connection and protocol classes.
  • Chores

    • Deprecated and removed obsolete files and scripts, consolidating device communication logic into new, maintainable modules.

Copy link

coderabbitai bot commented May 2, 2025

Walkthrough

This update introduces a major refactor and modularization of the bio-signal acquisition and streaming system. The legacy subprocess-based and stateful connection management for USB, WiFi, and BLE devices is replaced by a unified, asynchronous, and object-oriented approach. New modules (connection.py, chords_ble.py, chords_serial.py, chords_wifi.py) encapsulate protocol-specific logic, while the Flask backend (app.py) exposes simplified API endpoints for scanning, connecting, and managing device streams and applications. The web UI (index.html, style.css) is overhauled for a modern, responsive experience with protocol buttons, BLE device selection, CSV logging toggle, and app control. Disconnection detection and graceful shutdown are added across all real-time plotting and monitoring applications.

Changes

Files / Groups Change Summary
app.py Refactored to use a modular, asynchronous connection model via a new Connection class; replaced subprocess/thread-based device and stream management with thread-based, explicit JSON API endpoints for device scanning, connection, CSV logging, and app control; removed SSE, session-based device selection, and legacy error handling.
beetle.py, emgenvelope.py, eog.py, ffteeg.py,
gui.py, heartbeat_ecg.py, keystroke.py
Added LSL stream disconnection detection via timeout and graceful shutdown logic in real-time plotting/monitoring GUIs and scripts.
chords.py, npg-ble.py Deleted legacy scripts for Arduino serial and BLE device streaming; all logic now modularized and managed via new connection modules.
chords_ble.py New module for BLE device scanning, connection, and data streaming using Bleak; includes sample loss detection, async connection monitoring, and CLI interface.
chords_serial.py New module for USB serial device detection, connection, and data streaming; includes hardware handshake, packet parsing, and signal-based cleanup.
chords_wifi.py New module for WiFi/WebSocket device connection and data streaming with error detection and rate calculation.
connection.py New unified connection manager class for USB, WiFi, and BLE; handles LSL streaming, optional CSV logging, and protocol-specific connection logic; exposes CLI interface.
static/style.css Major UI/UX overhaul: new protocol/disconnect button styles, CSV toggle, BLE popup, app button enhancements, tooltips, and responsive design improvements.
templates/index.html Overhauled UI: protocol selection, BLE device popup, CSV toggle, disconnect logic, app control buttons, and client-driven fetch-based backend interaction; removed server-side forms and SSE.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebUI
    participant FlaskAPI
    participant ConnectionManager
    participant Device (USB/WiFi/BLE)
    participant LSL
    participant AppScript

    User->>WebUI: Click protocol button (USB/WiFi/BLE)
    WebUI->>FlaskAPI: /set_csv (CSV toggle state)
    WebUI->>FlaskAPI: /connect or /connect_ble (with params)
    FlaskAPI->>ConnectionManager: connect_usb/connect_wifi/connect_ble
    ConnectionManager->>Device: Establish connection, start data stream
    Device-->>ConnectionManager: Stream samples
    ConnectionManager->>LSL: Push samples to LSL
    ConnectionManager->>FlaskAPI: Connection status
    FlaskAPI->>WebUI: JSON response (success/failure)
    WebUI->>WebUI: Enable app buttons, update status

    User->>WebUI: Click app button
    WebUI->>FlaskAPI: /run_application (app name)
    FlaskAPI->>AppScript: Start app subprocess
    AppScript-->>FlaskAPI: Running status
    FlaskAPI->>WebUI: JSON response (app running)

    User->>WebUI: Click disconnect
    WebUI->>FlaskAPI: /stop_application (all apps), /disconnect
    FlaskAPI->>ConnectionManager: Cleanup connections
    ConnectionManager->>Device: Stop stream, cleanup
    ConnectionManager->>LSL: Close outlet
    FlaskAPI->>WebUI: JSON response (disconnected)
Loading

Possibly related PRs

  • upsidedownlabs/Chords-Python#38: Refactored app.py to support BLE device scanning and connection using subprocess-based NPG BLE client; this PR replaces that approach with a modular, async design and removes the subprocess-based BLE client.
  • upsidedownlabs/Chords-Python#35: Adds NPG-LITE wired and wireless support with automatic stream detection and new UI buttons; related but superseded by this PR's architectural refactor removing legacy NPG BLE client.

Poem

🐇
Refactored wires, BLE and more,
Async connections at the core!
Protocol buttons, popups bright,
CSV toggles—logging right.
Apps now run with just a click,
Streams detected—disconnects quick!
Modular code, a hoppy delight—
The Chords are tuned and running light!


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (16)
emgenvelope.py (1)

132-138: Timer stops but inlet is left open – consider explicit cleanup

After detecting a disconnection you stop the QTimer and close the window, but the LSL StreamInlet object remains open.
Although closing the window ends the process in __main__, if this widget were embedded inside a larger Qt application the inlet would keep running in the background.

Add an explicit self.inlet.close_stream() (or set it to None) before shutting down to release the socket promptly.

gui.py (1)

36-43: UI shuts down gracefully, but a tiny race remains

timer.stop() and win.close() are executed from the timer thread-context.
If plot_lsl_data() has already returned its app and the main thread is about to call app.exec_(), a rapid disconnect could close the window before the event loop starts, leading to:

RuntimeError: wrapped C/C++ object of type QWidget has been deleted

Safest pattern is to emit a Qt signal or use QtCore.QTimer.singleShot(0, win.close) instead of closing synchronously from the timer callback.

This is optional but removes a rare crash on flaky networks.

chords_ble.py (1)

95-99: print_rate() resets the counter but never prints — dead code

The coroutine continuously sleeps and sets self.samples_received = 0, but the value is neither read nor logged, so sample-rate statistics are lost.
Either add a print() (or better, a proper logger) or drop the task to avoid unnecessary context-switches.

-            await asyncio.sleep(1)
-            self.samples_received = 0
+            await asyncio.sleep(1)
+            print(f"Samples/s: {self.samples_received}")
+            self.samples_received = 0
chords_wifi.py (4)

5-5: Remove unused SciPy imports

butter and filtfilt are never referenced in this module, so the extra dependency only increases install size and import time.

-from scipy.signal import butter, filtfilt
🧰 Tools
🪛 Ruff (0.8.2)

5-5: scipy.signal.butter imported but unused

Remove unused import

(F401)


5-5: scipy.signal.filtfilt imported but unused

Remove unused import

(F401)


66-74: Computed throughput metrics are unused

sps, fps, and bps are calculated every second but never consumed. Either log them (handy for debugging) or remove the dead code to save a few µs per loop.

-                    sps = self.calculate_rate(self.sample_size, elapsed_time)
-                    fps = self.calculate_rate(self.packet_size, elapsed_time)
-                    bps = self.calculate_rate(self.data_size, elapsed_time)
+                    # Uncomment for live-rate diagnostics
+                    # print(f"SPS={self.calculate_rate(self.sample_size, elapsed_time):.0f}, "
+                    #       f"FPS={self.calculate_rate(self.packet_size, elapsed_time):.0f}, "
+                    #       f"BPS={self.calculate_rate(self.data_size, elapsed_time):.0f}")
🧰 Tools
🪛 Ruff (0.8.2)

67-67: Local variable sps is assigned to but never used

Remove assignment to unused variable sps

(F841)


68-68: Local variable fps is assigned to but never used

Remove assignment to unused variable fps

(F841)


69-69: Local variable bps is assigned to but never used

Remove assignment to unused variable bps

(F841)


96-104: Don’t terminate the whole process from a library class

sys.exit() will kill whichever program imports this module. Prefer raising a domain-specific exception and let callers decide how to recover.

-                                print("\nError: Sample Lost")
-                                self.cleanup()
-                                sys.exit(1)
+                                raise RuntimeError("Sample lost – connection appears unreliable")

(Apply the same change for the duplicate-sample branch.)


116-124: Exception is swallowed during cleanup

The bare except masks real bugs and the unused variable e triggers Ruff F841. At least log the message.

-            except Exception as e:
-                pass
+            except Exception as e:
+                print(f"Error while closing WebSocket: {e}")
🧰 Tools
🪛 Ruff (0.8.2)

120-120: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

chords_serial.py (2)

61-62: Remove unused sampling_rate assignment

sampling_rate is read from supported_boards but never referenced afterwards.

-                    sampling_rate = self.supported_boards[self.board]["sampling_rate"]
🧰 Tools
🪛 Ruff (0.8.2)

61-61: Local variable sampling_rate is assigned to but never used

Remove assignment to unused variable sampling_rate

(F841)


148-149: cleanup() should not call sys.exit()

For a reusable library, forcing the interpreter to exit prevents callers (e.g., Flask backend) from recovering or cleaning up additional resources. Return gracefully or raise an exception instead.

-        print("Cleanup Completed.")
-        sys.exit(0)
+        print("Cleanup Completed.")
+        # Let caller decide whether to exit
connection.py (2)

7-8: Remove unused sys import

sys is not referenced in this file.

-import sys
🧰 Tools
🪛 Ruff (0.8.2)

7-7: sys imported but unused

Remove unused import: sys

(F401)


60-68: Open the CSV file with a context manager

Using with open(...) ensures the file handle is flushed and closed even if an exception occurs before __del__ executes.

-            self.csv_file = open(filename, 'w', newline='')
-            headers = ['Counter'] + [f'Channel{i+1}' for i in range(self.num_channels)]
-            self.csv_writer = csv.writer(self.csv_file)
-            self.csv_writer.writerow(headers)
+            with open(filename, 'w', newline='') as f:
+                self.csv_writer = csv.writer(f)
+                headers = ['Counter'] + [f'Channel{i+1}' for i in range(self.num_channels)]
+                self.csv_writer.writerow(headers)
+            # Re-open in append mode for the streaming loop
+            self.csv_file = open(filename, 'a', newline='')
+            self.csv_writer = csv.writer(self.csv_file)
🧰 Tools
🪛 Ruff (0.8.2)

63-63: Use a context manager for opening files

(SIM115)

static/style.css (3)

333-344: .header h1 rules are duplicated – keep a single source of truth

These lines repeat the exact style block already defined at ~35-46. Duplicate selectors bloat the sheet and increase the risk of the two blocks getting out-of-sync in future edits.

-/* REMOVE duplicated .header h1 block (already defined above) */
-.header h1 {
-    font-size: 48px;
-    font-weight: bold;
-    color: #333;
-    margin: 0 0 15px 0;
-    background: linear-gradient(90deg, #ec4899, #a855f7, #3b82f6, #ec4899);
-    background-size: 300%;
-    -webkit-background-clip: text;
-    background-clip: text;
-    -webkit-text-fill-color: transparent;
-    animation: scroll-gradient 5s linear infinite;
-}

452-475: protocol-button-group & protocol-buttons-combined button blocks appear twice

The definitions at 55-62 and here are identical. Consider extracting common button styling into one block and using utility classes or CSS variables to override only what differs (e.g. size in media queries).
This will slim the stylesheet and make future tweaks less error-prone.


287-308: Back-to-back identical @media (max-width: 600px) queries

Two consecutive media queries target the same breakpoint with overlapping rules for the same selectors. Merge them to reduce CSS size and ensure later edits remain consistent.

Example:

@media (max-width: 600px) {
-    /* first block … */
-}
-
-@media (max-width: 600px) {
-    /* second block … */
+    /* merged declarations … */
 }
app.py (1)

172-176: Terminated subprocesses may linger as zombies

running_apps[app_name].terminate() sends SIGTERM but you never wait() on the process. On POSIX systems this leaves a zombie until the parent collects the exit status.

running_apps[app_name].terminate()
-running_apps.pop(app_name, None)
+running_apps[app_name].wait(timeout=5)
+running_apps.pop(app_name, None)

Optionally use kill() after a grace period.

templates/index.html (1)

136-141: CSV toggle is permanently disabled after initiating a connection

setCSVToggleState(false) is called at the start of every connect function but never reset to true upon successful connection, so users cannot toggle CSV logging for subsequent sessions without refreshing.

Add a call after a successful connection:

// inside success branch
enableAllAppButtons();
setCSVToggleState(true);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b503e6 and 1bd152a.

📒 Files selected for processing (16)
  • app.py (1 hunks)
  • beetle.py (7 hunks)
  • chords.py (0 hunks)
  • chords_ble.py (1 hunks)
  • chords_serial.py (1 hunks)
  • chords_wifi.py (1 hunks)
  • connection.py (1 hunks)
  • emgenvelope.py (4 hunks)
  • eog.py (3 hunks)
  • ffteeg.py (4 hunks)
  • gui.py (5 hunks)
  • heartbeat_ecg.py (4 hunks)
  • keystroke.py (2 hunks)
  • npg-ble.py (0 hunks)
  • static/style.css (3 hunks)
  • templates/index.html (2 hunks)
💤 Files with no reviewable changes (2)
  • chords.py
  • npg-ble.py
🧰 Additional context used
🧬 Code Graph Analysis (2)
keystroke.py (1)
eog.py (1)
  • detect_blinks (146-165)
chords_wifi.py (2)
chords_ble.py (2)
  • connect (161-172)
  • cleanup (151-159)
chords_serial.py (1)
  • cleanup (138-149)
🪛 Ruff (0.8.2)
chords_ble.py

75-75: Local variable channels is assigned to but never used

Remove assignment to unused variable channels

(F841)

chords_serial.py

61-61: Local variable sampling_rate is assigned to but never used

Remove assignment to unused variable sampling_rate

(F841)

connection.py

7-7: sys imported but unused

Remove unused import: sys

(F401)


63-63: Use a context manager for opening files

(SIM115)

chords_wifi.py

5-5: scipy.signal.butter imported but unused

Remove unused import

(F401)


5-5: scipy.signal.filtfilt imported but unused

Remove unused import

(F401)


67-67: Local variable sps is assigned to but never used

Remove assignment to unused variable sps

(F841)


68-68: Local variable fps is assigned to but never used

Remove assignment to unused variable fps

(F841)


69-69: Local variable bps is assigned to but never used

Remove assignment to unused variable bps

(F841)


120-120: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


135-135: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

app.py

5-5: webbrowser imported but unused

Remove unused import: webbrowser

(F401)

🔇 Additional comments (24)
ffteeg.py (4)

12-12: Added time module import for disconnection detection.

Adds the time module to enable tracking timestamps for disconnection detection.


21-22: Added stream state tracking for disconnection detection.

Good addition of variables to track stream state and detect disconnections:

  • stream_active flag to maintain stream status
  • last_data_time to track when data was last received

This enables graceful shutdown on disconnection.


115-115: Updated timestamp tracking for disconnection detection.

Records the current time whenever data is received from the LSL stream, supporting the disconnection detection mechanism.


137-143: Added disconnection detection and graceful shutdown.

Good implementation of disconnection detection logic:

  1. Checks if more than 2 seconds have passed since last data reception
  2. Sets the stream status flag to inactive
  3. Provides console feedback
  4. Stops the UI update timer
  5. Gracefully closes the application window

This prevents the application from freezing or displaying stale data when the stream is lost.

beetle.py (11)

5-5: Added sys module import for error handling.

The sys module is necessary for the sys.exit() calls added for proper error handling.


16-18: Added stream state tracking variables.

Good addition of variables to track stream state:

  • last_data_time to record timestamp of last received data
  • stream_active flag to maintain connection status
  • inlet explicit initialization to None for better error handling

22-22: Added proper error handling for missing streams.

Improved error handling by properly exiting when no LSL streams are found rather than continuing execution.


37-37: Added proper error handling for connection failure.

Ensured the application properly exits when unable to connect to any stream.


81-84: Added event handling for graceful exit.

Good addition of event handling during message display to allow users to close the window at any time, ensuring the application exits cleanly rather than hanging.


113-113: Added global declaration for last_data_time.

Ensures the global variable is properly accessed within the calibration function.


135-135: Added timestamp tracking during calibration.

Records the time when data is received during calibration to support disconnection detection.


138-142: Added disconnection detection during calibration.

Good implementation of disconnection detection during the critical calibration phase:

  1. Checks if 2+ seconds passed since last data reception
  2. Shows a user-friendly message
  3. Properly cleans up pygame resources
  4. Exits with an error code

144-147: Added event handling during calibration.

Important addition of event handling to allow users to exit during the calibration process.


197-197: Added timestamp tracking in main game loop.

Records the time when data is received in main game loop to support disconnection detection.


200-204: Added disconnection detection in main game loop.

Good implementation of disconnection detection during the main game loop:

  1. Checks if 2+ seconds passed since last data reception
  2. Shows a user-friendly message
  3. Sets the running flag to false to exit the loop gracefully
  4. Uses break to immediately exit the loop
heartbeat_ecg.py (4)

10-10: Added time module import for disconnection detection.

Adds the time module to enable tracking timestamps for disconnection detection.


19-20: Added stream state tracking for disconnection detection.

Good addition of variables to track stream state and detect disconnections:

  • stream_active flag to maintain stream status
  • last_data_time to track when data was last received

This enables graceful shutdown on disconnection.


114-114: Updated timestamp tracking for disconnection detection.

Records the current time whenever data is received from the LSL stream, supporting the disconnection detection mechanism.


128-133: Added disconnection detection and graceful shutdown.

Good implementation of disconnection detection logic:

  1. Checks if more than 2 seconds have passed since last data reception
  2. Sets the stream status flag to inactive
  3. Provides console feedback
  4. Stops the UI update timer
  5. Gracefully closes the application window

This prevents the application from freezing or displaying stale data when the stream is lost.

eog.py (3)

17-18: Added stream state tracking for disconnection detection.

Good addition of variables to track stream state and detect disconnections:

  • stream_active flag to maintain stream status
  • last_data_time to track when data was last received

This enables graceful shutdown on disconnection.


105-105: Updated timestamp tracking for disconnection detection.

Records the current time whenever data is received from the LSL stream, supporting the disconnection detection mechanism.


139-144: Added disconnection detection and graceful shutdown.

Good implementation of disconnection detection logic:

  1. Checks if more than 2 seconds have passed since last data reception
  2. Sets the stream status flag to inactive
  3. Provides console feedback
  4. Stops the UI update timer
  5. Gracefully closes the application window

This prevents the application from freezing or displaying stale data when the stream is lost.

static/style.css (1)

7-12: Avoid overflow: hidden on <body> – it prevents scrolling on small screens

Locking the viewport with height: 100vh; overflow: hidden; can make the entire UI unreachable / un-scrollable on devices where the content vertically exceeds 100 % of the viewport (mobile browsers add a dynamic URL bar, virtual keyboard, etc.).
Consider either:

-body {
-    height: 100vh;
-    overflow: hidden;
+body {
+    min-height: 100vh;   /* allow taller content */
+    overflow-y: auto;    /* enable scrolling when needed */
 }

or moving the height/overflow constraints to a subordinate wrapper that truly needs to be fixed-height (e.g. a splash screen).

templates/index.html (1)

513-578: Client expects /disconnect – update JS once the backend route is added

Once you add the Flask /disconnect route (see backend comment), remember to check the response here and surface errors to the user instead of refreshing regardless of failure.

const resp = await fetch('/disconnect', { method: 'POST' });
const data = await resp.json();
if (data.status !== 'success') {
    console.error(data.message);
    statusDiv.textContent = data.message || 'Disconnection failed';
    statusDiv.className = 'error-status';
    return; // skip reload
}

Comment on lines +108 to +110
global last_data_time, stream_active
if self.inlet is None or not self.stream_active:
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unnecessary global declaration to prevent namespace confusion

last_data_time and stream_active are instance attributes (self.last_data_time, self.stream_active).
Declaring them as global does nothing useful here and may trick future readers (or static-analysis tools) into thinking the variables live at module scope.

-        global last_data_time, stream_active

No other change is required—the rest of the method already uses the instance attributes correctly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
global last_data_time, stream_active
if self.inlet is None or not self.stream_active:
return
if self.inlet is None or not self.stream_active:
return

Comment on lines +70 to 83
self.last_data_time = time.time()
for sample in samples:
self.eog_data[self.current_index] = sample[0] # Store sample in circular buffer at current position
self.current_index = (self.current_index + 1) % self.buffer_size # Update index with wrap-around using modulo

filtered_eog = lfilter(self.b, self.a, self.eog_data)
self.detect_blinks(filtered_eog) # Run blink detection on the filtered signal
else:
if self.last_data_time and (time.time() - self.last_data_time) > 2:
print("LSL Status: Stream disconnected. Stopping detection.")
self.stop_detection()
popup.after(0, popup.quit) # Schedule quit on main thread
break # Exit the loop
except Exception as e:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

stream_active never updated & risk of false-positive disconnect

You introduced self.last_data_time but never toggle the new self.stream_active flag; meanwhile the disconnect check executes even before the first sample arrives, which can mark the stream as “disconnected” during normal start-up latency.

Suggested fixes:

-                samples, _ = self.inlet.pull_chunk(timeout=1.0, max_samples=1)
+                # Wait slightly longer on the very first pull to avoid premature time-outs
+                samples, _ = self.inlet.pull_chunk(timeout=2.0, max_samples=1)

@@
-                    self.last_data_time = time.time()
+                    self.last_data_time = time.time()
+                    self.stream_active   = True
@@
-                    if self.last_data_time and (time.time() - self.last_data_time) > 2:
+                    if self.last_data_time and (time.time() - self.last_data_time) > 2:
                         print("LSL Status: Stream disconnected. Stopping detection.")
+                        self.stream_active = False

Keeping stream_active in sync and giving the first pull a longer timeout will reduce spurious disconnects.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.last_data_time = time.time()
for sample in samples:
self.eog_data[self.current_index] = sample[0] # Store sample in circular buffer at current position
self.current_index = (self.current_index + 1) % self.buffer_size # Update index with wrap-around using modulo
filtered_eog = lfilter(self.b, self.a, self.eog_data)
self.detect_blinks(filtered_eog) # Run blink detection on the filtered signal
else:
if self.last_data_time and (time.time() - self.last_data_time) > 2:
print("LSL Status: Stream disconnected. Stopping detection.")
self.stop_detection()
popup.after(0, popup.quit) # Schedule quit on main thread
break # Exit the loop
except Exception as e:
self.last_data_time = time.time()
self.stream_active = True
for sample in samples:
self.eog_data[self.current_index] = sample[0] # Store sample in circular buffer at current position
self.current_index = (self.current_index + 1) % self.buffer_size # Update index with wrap-around using modulo
filtered_eog = lfilter(self.b, self.a, self.eog_data)
self.detect_blinks(filtered_eog) # Run blink detection on the filtered signal
else:
if self.last_data_time and (time.time() - self.last_data_time) > 2:
print("LSL Status: Stream disconnected. Stopping detection.")
self.stream_active = False
self.stop_detection()
popup.after(0, popup.quit) # Schedule quit on main thread
break # Exit the loop
except Exception as e:

Comment on lines +75 to +81
channels = [
int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]

self.samples_received += 1

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unused channels variable — either publish or remove

channels is decoded but never referenced, triggering the Ruff warning (F841) and wasting CPU cycles.

If you only need the counter logic, remove the assignment; if channel data will feed another pipeline, store it or emit an event.

-        channels = [
-            int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
-            int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
-            int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]
-
-        self.samples_received += 1
+        # Channel bytes decoded here when downstream processing is added
+        self.samples_received += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
channels = [
int.from_bytes(sample_data[1:3], byteorder='big', signed=True),
int.from_bytes(sample_data[3:5], byteorder='big', signed=True),
int.from_bytes(sample_data[5:7], byteorder='big', signed=True)]
self.samples_received += 1
# Channel bytes decoded here when downstream processing is added
self.samples_received += 1
🧰 Tools
🪛 Ruff (0.8.2)

75-75: Local variable channels is assigned to but never used

Remove assignment to unused variable channels

(F841)

Comment on lines +40 to +59
try:
while True:
try:
data = self.ws.recv()
self.last_data_time = time.time() # Update when data is received
except (websocket.WebSocketConnectionClosedException, ConnectionResetError) as e:
print(f"\nConnection closed: {str(e)}")
self.cleanup()
return
except Exception as e:
print(f"\n[ERROR] Connection error: {e}")
self.cleanup()
return

# Check for timeout (device stopped sending data)
if time.time() - self.last_data_time > self.timeout_sec:
print("\nDevice stopped sending data")
self.cleanup()
return

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ws.recv() blocks forever, defeating the timeout check

websocket-client waits indefinitely if no data arrives, so the subsequent timeout guard is never reached.
Set a socket timeout right after connecting and handle WebSocketTimeoutException instead.

 self.ws = websocket.WebSocket()
 self.ws.connect(f"ws://{host_ip}:81")
+self.ws.settimeout(self.timeout_sec)          # <-- keep recv responsive
-                except (websocket.WebSocketConnectionClosedException, ConnectionResetError) as e:
+                except (websocket.WebSocketTimeoutException,) as e:
+                    # No data within timeout
+                    print("\nDevice stopped sending data")
+                    self.cleanup()
+                    return
+                except (websocket.WebSocketConnectionClosedException, ConnectionResetError) as e:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
while True:
try:
data = self.ws.recv()
self.last_data_time = time.time() # Update when data is received
except (websocket.WebSocketConnectionClosedException, ConnectionResetError) as e:
print(f"\nConnection closed: {str(e)}")
self.cleanup()
return
except Exception as e:
print(f"\n[ERROR] Connection error: {e}")
self.cleanup()
return
# Check for timeout (device stopped sending data)
if time.time() - self.last_data_time > self.timeout_sec:
print("\nDevice stopped sending data")
self.cleanup()
return
self.ws = websocket.WebSocket()
self.ws.connect(f"ws://{host_ip}:81")
self.ws.settimeout(self.timeout_sec) # <-- keep recv responsive
try:
while True:
try:
data = self.ws.recv()
self.last_data_time = time.time() # Update when data is received
except (websocket.WebSocketTimeoutException,) as e:
# No data within timeout
print("\nDevice stopped sending data")
self.cleanup()
return
except (websocket.WebSocketConnectionClosedException, ConnectionResetError) as e:
print(f"\nConnection closed: {str(e)}")
self.cleanup()
return
except Exception as e:
print(f"\n[ERROR] Connection error: {e}")
self.cleanup()
return
# Check for timeout (device stopped sending data)
if time.time() - self.last_data_time > self.timeout_sec:
print("\nDevice stopped sending data")
self.cleanup()
return

Comment on lines +160 to +182
try:
print("\nConnected! (Press Ctrl+C to stop)")
while True:
data = self.wifi_connection.ws.recv()

if isinstance(data, (bytes, list)):
for i in range(0, len(data), self.wifi_connection.block_size):
block = data[i:i + self.wifi_connection.block_size]
if len(block) < self.wifi_connection.block_size:
continue

channel_data = []
for ch in range(self.wifi_connection.channels):
offset = 1 + ch * 2
sample = int.from_bytes(block[offset:offset + 2], byteorder='big', signed=True)
channel_data.append(sample)

if self.lsl_connection: # Push to LSL
self.lsl_connection.push_sample(channel_data)
if self.csv_logging and self.csv_writer: # Only log if CSV is set up
self.log_to_csv(channel_data)

except KeyboardInterrupt:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

No error handling for broken WebSocket inside WiFi loop

If the connection drops, ws.recv() raises WebSocketConnectionClosedException, crashing the loop. Surround the body with try/except similar to chords_wifi.process_data() or delegate to that method instead of duplicating code.

Comment on lines +183 to +184
self.wifi_connection.disconnect()
print("\nDisconnected")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

disconnect() is undefined for Chords_WIFI

Chords_WIFI only exposes cleanup(). Calling a non-existent method will raise AttributeError on the first keyboard interrupt.

-            self.wifi_connection.disconnect()
+            self.wifi_connection.cleanup()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.wifi_connection.disconnect()
print("\nDisconnected")
self.wifi_connection.cleanup()
print("\nDisconnected")

Comment on lines 196 to 197
if __name__ == "__main__":
app.run(debug=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Frontend calls a /disconnect endpoint that is missing here

templates/index.html → disconnectAll() issues a POST /disconnect, but no corresponding Flask route exists.
Without it, the call returns 404 and any server-side cleanup (stopping LSL stream, closing serial/BLE sockets, resetting globals) never happens.

Suggested minimal implementation:

+@app.route('/disconnect', methods=['POST'])
+def disconnect():
+    global active_connection, lsl_stream_active
+
+    try:
+        if active_connection:
+            active_connection.close()      # assuming `Connection` exposes close()
+            active_connection = None
+        lsl_stream_active = False
+        return jsonify({'status': 'success'})
+    except Exception as exc:
+        return jsonify({'status': 'error', 'message': str(exc)}), 500

Be sure the Connection class provides a safe close() / disconnect() method that stops background threads and releases resources.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if __name__ == "__main__":
app.run(debug=True)
@app.route('/disconnect', methods=['POST'])
def disconnect():
global active_connection, lsl_stream_active
try:
if active_connection:
active_connection.close() # assuming `Connection` exposes close()
active_connection = None
lsl_stream_active = False
return jsonify({'status': 'success'})
except Exception as exc:
return jsonify({'status': 'error', 'message': str(exc)}), 500
if __name__ == "__main__":
app.run(debug=True)
🧰 Tools
🪛 ast-grep (0.31.1)

[warning] 196-196: Detected Flask app with debug=True. Do not deploy to production with this flag enabled as it will leak sensitive information. Instead, consider using Flask configuration variables or setting 'debug' using system environment variables.
Context: app.run(debug=True)
Note: [CWE-489] Active Debug Code. [REFERENCES]
- https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/

(debug-enabled-python)

import asyncio
from connection import Connection
from threading import Thread
import webbrowser
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused webbrowser import

webbrowser is imported but never referenced, triggering Ruff F401 and adding unnecessary memory overhead at startup.

-from threading import Thread
-import webbrowser
+from threading import Thread
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import webbrowser
from threading import Thread
🧰 Tools
🪛 Ruff (0.8.2)

5-5: webbrowser imported but unused

Remove unused import: webbrowser

(F401)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
templates/index.html (1)

6-8: Title update and icon library version
The <title> refactor looks good. For the Font Awesome import, consider upgrading to a more recent major version (5.x or 6.x) to benefit from additional icons and security patches.

static/style.css (4)

7-8: Revisit overflow: hidden on <body>
Hiding overflow on the <body> can prevent scrolling when content (e.g., the BLE popup) exceeds viewport height on smaller screens. Consider removing or toggling it only when needed (e.g., when the popup is shown).


35-46: Duplicate .header h1 styles
The gradient-animated header text rules are defined twice (lines 35–46 and 343–354). Consolidate into a single block to avoid maintenance overhead.

Also applies to: 343-354


55-61: Consolidate duplicated .protocol-button-group rules
The .protocol-button-group selector appears twice with identical properties. Merge them into one definition to DRY up the CSS.

Also applies to: 462-468


275-300: Duplicate media queries for max-width: 600px
Two nearly identical blocks target the same breakpoint. Combine these into a single @media (max-width: 600px) section to avoid inconsistencies.

Also applies to: 302-323

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bd152a and 617e87e.

📒 Files selected for processing (2)
  • static/style.css (3 hunks)
  • templates/index.html (2 hunks)

Comment on lines +131 to +135
// Initialize CSV toggle
document.getElementById('csvToggle').addEventListener('change', function() {
fetch('/set_csv', {
method: 'POST',
headers: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing CSV toggle element (csvToggle)
The script at DOMContentLoaded references document.getElementById('csvToggle'), but there is no element with that ID in the markup. This will throw at runtime. Either add the corresponding <input type="checkbox" id="csvToggle"> or update the JS to use the correct control (e.g., csvRecordBtn).

Comment on lines +143 to +148
// function setCSVToggleState(enabled) {
// const csvToggle = document.getElementById('csvToggle');
// csvToggle.disabled = !enabled;
// csvToggle.parentElement.style.opacity = enabled ? '1' : '0.6';
// csvToggle.parentElement.style.cursor = enabled ? 'pointer' : 'not-allowed';
// }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Commented-out setCSVToggleState causes broken calls
You call setCSVToggleState(false) in the BLE/USB/WiFi flows but the function is commented out. Uncomment or remove these calls to avoid JavaScript errors when attempting to disable/enable the CSV toggle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant