Skip to content

Commit 187e58b

Browse files
committed
Fully Documented Code
1 parent 34ba1a8 commit 187e58b

File tree

2 files changed

+187
-33
lines changed

2 files changed

+187
-33
lines changed

chordspy/app.py

Lines changed: 138 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,73 @@
1-
from flask import Flask, render_template, request, jsonify
2-
from chordspy.connection import Connection
3-
import threading
4-
import asyncio
5-
import logging
6-
from bleak import BleakScanner
7-
from flask import Response
8-
import queue
9-
import yaml
10-
from pathlib import Path
11-
import os
12-
import webbrowser
13-
import logging
1+
"""
2+
Flask-based web interface for managing connections to devices and applications.
3+
This module provides a web-based GUI for:
4+
- Scanning and connecting to devices via USB, WiFi, or BLE
5+
- Managing data streaming and recording
6+
- Launching and monitoring Chords-Python applications
7+
- Displaying real-time console updates
8+
- Handling error logging
9+
The application uses Server-Sent Events (SSE) for real-time updates to the frontend.
10+
"""
1411

15-
console_queue = queue.Queue()
16-
app = Flask(__name__)
17-
logging.basicConfig(level=logging.INFO)
12+
# Importing Necessary Libraries
13+
from flask import Flask, render_template, request, jsonify # Flask web framework
14+
from chordspy.connection import Connection # Connection management module
15+
import threading # For running connection management in a separate thread
16+
import asyncio # For asynchronous operations, especially with BLE
17+
import logging # For logging errors and information
18+
from bleak import BleakScanner # BLE device scanner from Bleak library
19+
from flask import Response # For handling server-sent events (SSE)
20+
import queue # Queue for managing console messages
21+
import yaml # For loading application configuration from YAML files
22+
from pathlib import Path # For handling file paths in a platform-independent way
23+
import os # For file and directory operations
24+
import webbrowser # For opening the web interface in a browser
25+
import logging # For logging errors and information
26+
27+
console_queue = queue.Queue() # Global queue for console messages to be displayed in the web interface
28+
app = Flask(__name__) # Initialize Flask application
29+
logging.basicConfig(level=logging.INFO) # Configure logging
1830
log = logging.getLogger('werkzeug')
19-
log.setLevel(logging.ERROR) # Only show errors
31+
log.setLevel(logging.ERROR) # Only show errors from Werkzeug (Flask's WSGI)
2032

2133
# Global variables
22-
connection_manager = None
23-
connection_thread = None
24-
ble_devices = []
25-
stream_active = False
26-
running_apps = {} # Dictionary to track running apps
34+
connection_manager = None # Manages the device connection
35+
connection_thread = None # Thread for connection management
36+
ble_devices = [] # List of discovered BLE devices
37+
stream_active = False # Flag indicating if data stream is active
38+
running_apps = {} # Dictionary to track running applications
2739

40+
# Error logging endpoint. This allows the frontend to send error messages to be logged.
2841
@app.route('/log_error', methods=['POST'])
2942
def log_error():
43+
"""
44+
Endpoint for logging errors from the frontend. It receives error data via POST request and writes it to a log file.
45+
Returns:
46+
JSON response with status and optional error message.
47+
"""
3048
try:
3149
error_data = request.get_json()
3250
if not error_data or 'error' not in error_data or 'log_error' in str(error_data):
3351
return jsonify({'status': 'error', 'message': 'Invalid data'}), 400
3452

35-
os.makedirs('logs', exist_ok=True)
53+
os.makedirs('logs', exist_ok=True) # Ensure logs directory exists
3654

37-
with open('logs/logging.txt', 'a') as f:
55+
with open('logs/logging.txt', 'a') as f: # Append error to log file
3856
f.write(error_data['error'])
3957

4058
return jsonify({'status': 'success'})
4159
except Exception as e:
4260
return jsonify({'status': 'error', 'message': 'Logging failed'}), 500
4361

62+
# Decorator to run async functions in a synchronous context. It allows us to call async functions from Flask routes.
4463
def run_async(coro):
64+
"""
65+
Decorator to run async functions in a synchronous context.
66+
Args:
67+
coro: The coroutine to be executed.
68+
Returns:
69+
A wrapper function that runs the coroutine in a new event loop.
70+
"""
4571
def wrapper(*args, **kwargs):
4672
loop = asyncio.new_event_loop()
4773
asyncio.set_event_loop(loop)
@@ -51,12 +77,20 @@ def wrapper(*args, **kwargs):
5177
loop.close()
5278
return wrapper
5379

80+
# Main route for the web interface. It renders the index.html template.
5481
@app.route('/')
5582
def index():
83+
"""Render the main index page of the web interface."""
5684
return render_template('index.html')
5785

86+
# Route to retrieve the configuration for available Chord-Python applications.
5887
@app.route('/get_apps_config')
5988
def get_apps_config():
89+
"""
90+
Retrieve the configuration for available applications.It looks for apps.yaml in either the package config directory or a local config directory.
91+
Returns:
92+
JSON response containing the application configuration or an empty list if not found.
93+
"""
6094
try:
6195
config_path = Path(__file__).parent / 'config' / 'apps.yaml' # Try package-relative path first
6296
if not config_path.exists():
@@ -72,9 +106,15 @@ def get_apps_config():
72106
logging.error(f"Error loading apps config: {str(e)}")
73107
return jsonify({'apps': [], 'error': str(e)})
74108

109+
# Route to scan for nearby BLE devices. It uses BleakScanner to discover devices.
75110
@app.route('/scan_ble')
76111
@run_async
77112
async def scan_ble_devices():
113+
"""
114+
Scan for nearby BLE devices. It uses BleakScanner to discover devices for 5 seconds and filters for devices with names starting with 'NPG' or 'npg'.
115+
Returns:
116+
JSON response with list of discovered devices or error message.
117+
"""
78118
global ble_devices
79119
try:
80120
devices = await BleakScanner.discover(timeout=5)
@@ -85,36 +125,67 @@ async def scan_ble_devices():
85125
logging.error(f"BLE scan error: {str(e)}")
86126
return jsonify({'status': 'error', 'message': str(e)}), 500
87127

128+
# Route to check if the data stream is currently active. It checks the connection manager's stream_active flag.
88129
@app.route('/check_stream')
89130
def check_stream():
131+
"""
132+
Check if data stream is currently active.
133+
Returns:
134+
JSON response with connection status.
135+
"""
90136
is_connected = connection_manager.stream_active if hasattr(connection_manager, 'stream_active') else False
91137
return jsonify({'connected': is_connected})
92138

139+
# Route to check the current connection status with the device. It returns 'connected' if the stream is active, otherwise 'connecting'.
93140
@app.route('/check_connection')
94141
def check_connection():
142+
"""
143+
Check the current connection status with the device.
144+
Returns:
145+
JSON response with connection status ('connected' or 'connecting').
146+
"""
95147
if connection_manager and connection_manager.stream_active:
96148
return jsonify({'status': 'connected'})
97149
return jsonify({'status': 'connecting'})
98150

151+
# Function to post messages to the console queue. It updates the stream_active flag based on the message content. This function is used to send messages to the web interface for display in real-time.
99152
def post_console_message(message):
153+
"""
154+
Post a message to the console queue for display in the web interface and updates the stream_active flag based on message content.
155+
Args:
156+
message: The message to be displayed in the console.
157+
"""
100158
global stream_active
101159
if "LSL stream started" in message:
102160
stream_active = True
103161
elif "disconnected" in message:
104162
stream_active = False
105163
console_queue.put(message)
106164

165+
# Route for Server-Sent Events (SSE) to provide real-time console updates to the web interface.
107166
@app.route('/console_updates')
108167
def console_updates():
168+
"""
169+
Server-Sent Events (SSE) endpoint for real-time console updates.
170+
Returns:
171+
SSE formatted messages from the console queue.
172+
"""
109173
def event_stream():
174+
"""Generator function that yields messages from the console queue as SSE formatted messages."""
110175
while True:
111176
message = console_queue.get()
112177
yield f"data: {message}\n\n"
113178

114179
return Response(event_stream(), mimetype="text/event-stream")
115180

181+
# Route to launch Chord-Python application as a subprocess. It receives the application name via POST request and starts it as a Python module.
116182
@app.route('/launch_app', methods=['POST'])
117183
def launch_application():
184+
"""
185+
Launch a Chord-Python application as a subprocess.It receives the application name via POST request and starts it as a Python module.
186+
Returns:
187+
JSON response indicating success or failure of application launch.
188+
"""
118189
if not connection_manager or not connection_manager.stream_active:
119190
return jsonify({'status': 'error', 'message': 'No active stream'}), 400
120191

@@ -134,16 +205,23 @@ def launch_application():
134205

135206
# Run the module using Python's -m flag
136207
process = subprocess.Popen([sys.executable, "-m", f"chordspy.{module_name}"])
137-
138-
running_apps[module_name] = process
208+
running_apps[module_name] = process # Track running application
139209

140210
return jsonify({'status': 'success', 'message': f'Launched {module_name}'})
141211
except Exception as e:
142212
logging.error(f"Error launching {module_name}: {str(e)}")
143213
return jsonify({'status': 'error', 'message': str(e)}), 500
144214

215+
# Route to check the status of a running application. It checks if the application is in the running_apps dictionary and whether its process is still active.
145216
@app.route('/check_app_status/<app_name>')
146217
def check_app_status(app_name):
218+
"""
219+
Check the status of a running application.
220+
Args:
221+
app_name: Name of the application to check.
222+
Returns:
223+
JSON response indicating if the application is running or not.
224+
"""
147225
if app_name in running_apps:
148226
if running_apps[app_name].poll() is None: # Still running
149227
return jsonify({'status': 'running'})
@@ -152,8 +230,14 @@ def check_app_status(app_name):
152230
return jsonify({'status': 'not_running'})
153231
return jsonify({'status': 'not_running'})
154232

233+
# Route to connect to a device using the specified protocol. It supports USB, WiFi, and BLE connections. Starts connection in a separate thread.
155234
@app.route('/connect', methods=['POST'])
156235
def connect_device():
236+
"""
237+
Establish connection to a device using the specified protocol.It supports USB, WiFi, and BLE connections. Starts connection in a separate thread.
238+
Returns:
239+
JSON response indicating connection status.
240+
"""
157241
global connection_manager, connection_thread, stream_active
158242

159243
data = request.get_json()
@@ -173,6 +257,9 @@ def connect_device():
173257
connection_manager = Connection()
174258

175259
def run_connection():
260+
"""
261+
Internal function to handle the connection process in a thread.
262+
"""
176263
try:
177264
if protocol == 'usb':
178265
success = connection_manager.connect_usb()
@@ -202,8 +289,14 @@ def run_connection():
202289

203290
return jsonify({'status': 'connecting', 'protocol': protocol})
204291

292+
# Route to disconnect from the currently connected device. It cleans up the connection manager and resets the stream status.
205293
@app.route('/disconnect', methods=['POST'])
206294
def disconnect_device():
295+
"""
296+
Disconnect from the currently connected device.
297+
Returns:
298+
JSON response indicating disconnection status.
299+
"""
207300
global connection_manager, stream_active
208301
if connection_manager:
209302
connection_manager.cleanup()
@@ -212,8 +305,14 @@ def disconnect_device():
212305
return jsonify({'status': 'disconnected'})
213306
return jsonify({'status': 'no active connection'})
214307

308+
# Route to start recording data from the connected device to a CSV file.
215309
@app.route('/start_recording', methods=['POST'])
216310
def start_recording():
311+
"""
312+
Start recording data from the connected device to a CSV file.
313+
Returns:
314+
JSON response indicating recording status.
315+
"""
217316
global connection_manager
218317
if not connection_manager:
219318
return jsonify({'status': 'error', 'message': 'No active connection'}), 400
@@ -234,8 +333,14 @@ def start_recording():
234333
logging.error(f"Recording error: {str(e)}")
235334
return jsonify({'status': 'error', 'message': str(e)}), 500
236335

336+
# Route to stop the current recording session. It calls the stop_csv_recording method of the connection manager.
237337
@app.route('/stop_recording', methods=['POST'])
238338
def stop_recording():
339+
"""
340+
Stop the current recording session.
341+
Returns:
342+
JSON response indicating recording stop status.
343+
"""
239344
global connection_manager
240345
if connection_manager:
241346
try:
@@ -248,12 +353,17 @@ def stop_recording():
248353
return jsonify({'status': 'error', 'message': str(e)}), 500
249354
return jsonify({'status': 'error', 'message': 'No active connection'}), 400
250355

356+
# Route to check if a specific application is running. It checks the running_apps dictionary for the application's process.
251357
def main():
358+
"""
359+
Main entry point for the application. It starts the Flask server and opens the web browser to the application.
360+
"""
252361
def open_browser():
362+
"""Open the default web browser to the application URL."""
253363
webbrowser.open("http://localhost:5000")
254364

255-
threading.Timer(1.5, open_browser).start()
256-
app.run(debug=True, use_reloader=False, host='0.0.0.0', port=5000)
365+
threading.Timer(1, open_browser).start() # Open browser after 1 seconds to allow server to start
366+
app.run(debug=True, use_reloader=False, host='0.0.0.0', port=5000) # Start Flask application
257367

258368
if __name__ == "__main__":
259369
main()

0 commit comments

Comments
 (0)