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
+ """
14
11
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
18
30
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)
20
32
21
33
# 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
27
39
40
+ # Error logging endpoint. This allows the frontend to send error messages to be logged.
28
41
@app .route ('/log_error' , methods = ['POST' ])
29
42
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
+ """
30
48
try :
31
49
error_data = request .get_json ()
32
50
if not error_data or 'error' not in error_data or 'log_error' in str (error_data ):
33
51
return jsonify ({'status' : 'error' , 'message' : 'Invalid data' }), 400
34
52
35
- os .makedirs ('logs' , exist_ok = True )
53
+ os .makedirs ('logs' , exist_ok = True ) # Ensure logs directory exists
36
54
37
- with open ('logs/logging.txt' , 'a' ) as f :
55
+ with open ('logs/logging.txt' , 'a' ) as f : # Append error to log file
38
56
f .write (error_data ['error' ])
39
57
40
58
return jsonify ({'status' : 'success' })
41
59
except Exception as e :
42
60
return jsonify ({'status' : 'error' , 'message' : 'Logging failed' }), 500
43
61
62
+ # Decorator to run async functions in a synchronous context. It allows us to call async functions from Flask routes.
44
63
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
+ """
45
71
def wrapper (* args , ** kwargs ):
46
72
loop = asyncio .new_event_loop ()
47
73
asyncio .set_event_loop (loop )
@@ -51,12 +77,20 @@ def wrapper(*args, **kwargs):
51
77
loop .close ()
52
78
return wrapper
53
79
80
+ # Main route for the web interface. It renders the index.html template.
54
81
@app .route ('/' )
55
82
def index ():
83
+ """Render the main index page of the web interface."""
56
84
return render_template ('index.html' )
57
85
86
+ # Route to retrieve the configuration for available Chord-Python applications.
58
87
@app .route ('/get_apps_config' )
59
88
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
+ """
60
94
try :
61
95
config_path = Path (__file__ ).parent / 'config' / 'apps.yaml' # Try package-relative path first
62
96
if not config_path .exists ():
@@ -72,9 +106,15 @@ def get_apps_config():
72
106
logging .error (f"Error loading apps config: { str (e )} " )
73
107
return jsonify ({'apps' : [], 'error' : str (e )})
74
108
109
+ # Route to scan for nearby BLE devices. It uses BleakScanner to discover devices.
75
110
@app .route ('/scan_ble' )
76
111
@run_async
77
112
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
+ """
78
118
global ble_devices
79
119
try :
80
120
devices = await BleakScanner .discover (timeout = 5 )
@@ -85,36 +125,67 @@ async def scan_ble_devices():
85
125
logging .error (f"BLE scan error: { str (e )} " )
86
126
return jsonify ({'status' : 'error' , 'message' : str (e )}), 500
87
127
128
+ # Route to check if the data stream is currently active. It checks the connection manager's stream_active flag.
88
129
@app .route ('/check_stream' )
89
130
def check_stream ():
131
+ """
132
+ Check if data stream is currently active.
133
+ Returns:
134
+ JSON response with connection status.
135
+ """
90
136
is_connected = connection_manager .stream_active if hasattr (connection_manager , 'stream_active' ) else False
91
137
return jsonify ({'connected' : is_connected })
92
138
139
+ # Route to check the current connection status with the device. It returns 'connected' if the stream is active, otherwise 'connecting'.
93
140
@app .route ('/check_connection' )
94
141
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
+ """
95
147
if connection_manager and connection_manager .stream_active :
96
148
return jsonify ({'status' : 'connected' })
97
149
return jsonify ({'status' : 'connecting' })
98
150
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.
99
152
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
+ """
100
158
global stream_active
101
159
if "LSL stream started" in message :
102
160
stream_active = True
103
161
elif "disconnected" in message :
104
162
stream_active = False
105
163
console_queue .put (message )
106
164
165
+ # Route for Server-Sent Events (SSE) to provide real-time console updates to the web interface.
107
166
@app .route ('/console_updates' )
108
167
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
+ """
109
173
def event_stream ():
174
+ """Generator function that yields messages from the console queue as SSE formatted messages."""
110
175
while True :
111
176
message = console_queue .get ()
112
177
yield f"data: { message } \n \n "
113
178
114
179
return Response (event_stream (), mimetype = "text/event-stream" )
115
180
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.
116
182
@app .route ('/launch_app' , methods = ['POST' ])
117
183
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
+ """
118
189
if not connection_manager or not connection_manager .stream_active :
119
190
return jsonify ({'status' : 'error' , 'message' : 'No active stream' }), 400
120
191
@@ -134,16 +205,23 @@ def launch_application():
134
205
135
206
# Run the module using Python's -m flag
136
207
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
139
209
140
210
return jsonify ({'status' : 'success' , 'message' : f'Launched { module_name } ' })
141
211
except Exception as e :
142
212
logging .error (f"Error launching { module_name } : { str (e )} " )
143
213
return jsonify ({'status' : 'error' , 'message' : str (e )}), 500
144
214
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.
145
216
@app .route ('/check_app_status/<app_name>' )
146
217
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
+ """
147
225
if app_name in running_apps :
148
226
if running_apps [app_name ].poll () is None : # Still running
149
227
return jsonify ({'status' : 'running' })
@@ -152,8 +230,14 @@ def check_app_status(app_name):
152
230
return jsonify ({'status' : 'not_running' })
153
231
return jsonify ({'status' : 'not_running' })
154
232
233
+ # Route to connect to a device using the specified protocol. It supports USB, WiFi, and BLE connections. Starts connection in a separate thread.
155
234
@app .route ('/connect' , methods = ['POST' ])
156
235
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
+ """
157
241
global connection_manager , connection_thread , stream_active
158
242
159
243
data = request .get_json ()
@@ -173,6 +257,9 @@ def connect_device():
173
257
connection_manager = Connection ()
174
258
175
259
def run_connection ():
260
+ """
261
+ Internal function to handle the connection process in a thread.
262
+ """
176
263
try :
177
264
if protocol == 'usb' :
178
265
success = connection_manager .connect_usb ()
@@ -202,8 +289,14 @@ def run_connection():
202
289
203
290
return jsonify ({'status' : 'connecting' , 'protocol' : protocol })
204
291
292
+ # Route to disconnect from the currently connected device. It cleans up the connection manager and resets the stream status.
205
293
@app .route ('/disconnect' , methods = ['POST' ])
206
294
def disconnect_device ():
295
+ """
296
+ Disconnect from the currently connected device.
297
+ Returns:
298
+ JSON response indicating disconnection status.
299
+ """
207
300
global connection_manager , stream_active
208
301
if connection_manager :
209
302
connection_manager .cleanup ()
@@ -212,8 +305,14 @@ def disconnect_device():
212
305
return jsonify ({'status' : 'disconnected' })
213
306
return jsonify ({'status' : 'no active connection' })
214
307
308
+ # Route to start recording data from the connected device to a CSV file.
215
309
@app .route ('/start_recording' , methods = ['POST' ])
216
310
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
+ """
217
316
global connection_manager
218
317
if not connection_manager :
219
318
return jsonify ({'status' : 'error' , 'message' : 'No active connection' }), 400
@@ -234,8 +333,14 @@ def start_recording():
234
333
logging .error (f"Recording error: { str (e )} " )
235
334
return jsonify ({'status' : 'error' , 'message' : str (e )}), 500
236
335
336
+ # Route to stop the current recording session. It calls the stop_csv_recording method of the connection manager.
237
337
@app .route ('/stop_recording' , methods = ['POST' ])
238
338
def stop_recording ():
339
+ """
340
+ Stop the current recording session.
341
+ Returns:
342
+ JSON response indicating recording stop status.
343
+ """
239
344
global connection_manager
240
345
if connection_manager :
241
346
try :
@@ -248,12 +353,17 @@ def stop_recording():
248
353
return jsonify ({'status' : 'error' , 'message' : str (e )}), 500
249
354
return jsonify ({'status' : 'error' , 'message' : 'No active connection' }), 400
250
355
356
+ # Route to check if a specific application is running. It checks the running_apps dictionary for the application's process.
251
357
def main ():
358
+ """
359
+ Main entry point for the application. It starts the Flask server and opens the web browser to the application.
360
+ """
252
361
def open_browser ():
362
+ """Open the default web browser to the application URL."""
253
363
webbrowser .open ("http://localhost:5000" )
254
364
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
257
367
258
368
if __name__ == "__main__" :
259
369
main ()
0 commit comments