11
11
from flask import Response
12
12
13
13
app = Flask (__name__ )
14
- app .secret_key = 'your_secret_key_here '
14
+ app .secret_key = '-- '
15
15
16
- lsl_process = None
17
- lsl_running = False
18
- npg_running = False
19
- npg_process = None
20
- app_processes = {}
21
- current_message = None
22
- discovered_devices = []
23
- npg_connection_thread = None
16
+ lsl_process = None # Process for LSL
17
+ lsl_running = False # Flag to check if LSL is running
18
+ npg_running = False # Flag to check if NPG is running
19
+ npg_process = None # Process for NPG
20
+ app_processes = {} # Dictionary to hold other app processes
21
+ current_message = None # Message to display in the UI
22
+ discovered_devices = [] # List for all discovered devices
23
+ npg_connection_thread = None # Thread for NPG connection
24
24
25
25
def is_process_running (name ):
26
- for proc in psutil .process_iter (['pid' , 'name' ]):
27
- if name in proc .info ['name' ]:
28
- return True
29
- return False
26
+ """Function to check if a process is running by name."""
27
+ for proc in psutil .process_iter (['pid' , 'name' ]): # Iterate through all processes
28
+ if name in proc .info ['name' ]: # Check if the process name matches
29
+ return True # Returns True if process found
30
+ return False # Returns False if process not found
30
31
31
32
@app .route ("/" )
32
33
def home ():
34
+ """Render the home page with the current status of LSL Stream , NPG stream, running applications, messages."""
33
35
return render_template ("index.html" , lsl_started = lsl_running , npg_started = npg_running , running_apps = [k for k ,v in app_processes .items () if v .poll () is None ], message = current_message , devices = session .get ('devices' , []), selected_device = session .get ('selected_device' ))
34
36
35
37
@app .route ("/scan_devices" , methods = ["POST" ])
36
38
def scan_devices ():
39
+ """Scan for available devices using the npg-ble.py --scan script."""
37
40
global discovered_devices
38
41
39
42
try :
@@ -57,8 +60,8 @@ def scan_devices():
57
60
"address" : parts [1 ]
58
61
})
59
62
60
- session ['devices' ] = devices
61
- discovered_devices = devices
63
+ session ['devices' ] = devices # Store devices in session
64
+ discovered_devices = devices # Update devices list to global variable
62
65
return jsonify ({"status" : "success" , "devices" : devices })
63
66
64
67
except subprocess .TimeoutExpired :
@@ -70,48 +73,50 @@ def scan_devices():
70
73
71
74
@app .route ("/connect_device" , methods = ["POST" ])
72
75
def connect_device ():
76
+ """Handle POST request to connect to a Bluetooth device"""
73
77
global npg_process , npg_running , npg_connection_thread , current_message
74
78
75
- device_address = request .form .get ("device_address" )
76
- if not device_address :
79
+ device_address = request .form .get ("device_address" ) # Get device address from the POST form data
80
+ if not device_address : # Check if device address was provided
77
81
return jsonify ({"status" : "error" , "message" : "No device selected" })
78
82
79
- session ['selected_device' ] = device_address
83
+ session ['selected_device' ] = device_address # Store selected device in session
80
84
81
- if npg_connection_thread and npg_connection_thread .is_alive ():
82
- if npg_process and npg_process .poll () is None :
85
+ if npg_connection_thread and npg_connection_thread .is_alive (): # Check if there's an existing connection thread running
86
+ if npg_process and npg_process .poll () is None : # If any active NPG process, terminate it
83
87
npg_process .terminate ()
84
88
try :
85
- npg_process .wait (timeout = 2 )
89
+ npg_process .wait (timeout = 2 ) # Wait for process to terminate (with timeout)
86
90
except subprocess .TimeoutExpired :
87
- npg_process .kill ()
91
+ npg_process .kill () # Force kill if process doesn't terminate smoothly within timeout
88
92
89
93
def connect_and_monitor ():
94
+ """Connect to the device and monitor the output."""
90
95
global npg_process , npg_running , current_message
91
96
92
97
try :
93
- script_path = os .path .join (os .path .dirname (os .path .abspath (__file__ )), "npg-ble.py" )
98
+ script_path = os .path .join (os .path .dirname (os .path .abspath (__file__ )), "npg-ble.py" ) # Get the path of the npg.ble script to run
94
99
creation_flags = subprocess .CREATE_NO_WINDOW if sys .platform == "win32" else 0
95
- npg_process = subprocess .Popen ([sys .executable , script_path , "--connect" , device_address ], stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , bufsize = 1 , universal_newlines = True , creationflags = creation_flags )
100
+ npg_process = subprocess .Popen ([sys .executable , script_path , "--connect" , device_address ], stdout = subprocess .PIPE , stderr = subprocess .STDOUT , text = True , bufsize = 1 , universal_newlines = True , creationflags = creation_flags ) # Start the process with no window
96
101
time .sleep (1 )
97
102
98
103
# Monitor the output for connection status
99
104
connected = False
100
- for line in iter (npg_process .stdout .readline , '' ):
101
- if "Connected to" in line :
102
- connected = True
103
- if npg_process .poll () is not None :
105
+ for line in iter (npg_process .stdout .readline , '' ): # Read lines from the process output
106
+ if "Connected to" in line : # Check for the required message showing successful connection
107
+ connected = True # Set connected flag to True
108
+ if npg_process .poll () is not None : # If process has terminated, break the loop
104
109
break
105
110
106
- if connected :
111
+ if connected : # If connected successfully, update the global variables
107
112
current_message = f"Connected to { device_address } "
108
- npg_running = True
109
- monitor_thread = Thread (target = monitor_process_output , args = (npg_process , "npg" ), daemon = True )
110
- monitor_thread .start ()
113
+ npg_running = True # Set npg_running to True
114
+ monitor_thread = Thread (target = monitor_process_output , args = (npg_process , "npg" ), daemon = True ) # Create a new thread to monitor the process output
115
+ monitor_thread .start () # Start the monitoring thread that will handle the output of the process
111
116
else :
112
- current_message = f"Failed to connect to { device_address } "
113
- npg_running = False
114
- if npg_process .poll () is None :
117
+ current_message = f"Failed to connect to { device_address } " # If not connected, update the message
118
+ npg_running = False # Set npg_running to False
119
+ if npg_process .poll () is None : # If process is still running, terminate it
115
120
npg_process .terminate ()
116
121
117
122
except Exception as e :
@@ -128,6 +133,7 @@ def connect_and_monitor():
128
133
129
134
@app .route ("/check_connection" , methods = ["GET" ])
130
135
def check_connection ():
136
+ """Check the connection status of the NPG process"""
131
137
global npg_running , current_message , npg_process
132
138
133
139
if npg_process is None or npg_process .poll () is not None :
@@ -150,15 +156,17 @@ def check_connection():
150
156
return jsonify ({"connected" : True , "message" : current_message })
151
157
elif "Data Interrupted" in line or "Data Interrupted (Bluetooth disconnected)" in line :
152
158
npg_running = False
159
+ stop_dependent_apps ("npg" )
153
160
current_message = line .strip ()
154
161
return jsonify ({"connected" : False , "message" : current_message })
155
162
156
163
return jsonify ({"connected" : npg_running , "message" : current_message or "Connecting..." })
157
164
158
165
def monitor_process_output (process , process_type ):
166
+ """Monitor the output of a subprocess(LSL/NPG) and handle termination events."""
159
167
global lsl_running , npg_running , current_message , app_processes
160
168
161
- while True :
169
+ while True : # Infinite loop to keep checking the process output
162
170
if process .poll () is not None : # Process has terminated
163
171
if process_type == "lsl" :
164
172
lsl_running = False
@@ -168,27 +176,27 @@ def monitor_process_output(process, process_type):
168
176
current_message = "NPG stream terminated"
169
177
break
170
178
171
- line = process .stdout .readline ()
179
+ line = process .stdout .readline () # Read a line from the process output
172
180
if not line :
173
181
time .sleep (0.1 )
174
182
continue
175
183
176
184
print (f"{ process_type } output:" , line .strip ()) # Debug logging
177
185
178
- if process_type == "npg" and ("Data Interrupted" in line or "Data Interrupted (Bluetooth disconnected)" in line ):
186
+ if process_type == "npg" and ("Data Interrupted" in line or "Data Interrupted (Bluetooth disconnected)" in line . lower ()): # Handle NPG data interruption
179
187
current_message = "NPG connection lost - stopping all applications"
180
- npg_running = False
181
- stop_dependent_apps ("npg" )
188
+ npg_running = False # Set npg_running to False
189
+ stop_dependent_apps ("npg" ) # Stop all dependent applications
182
190
183
- if process .poll () is None :
191
+ if process .poll () is None : # If process is still running, terminate it
184
192
process .terminate ()
185
193
try :
186
194
process .wait (timeout = 0.5 )
187
195
except subprocess .TimeoutExpired :
188
196
process .kill ()
189
197
break
190
198
191
- elif process_type == "lsl" and ("Error while closing serial connection" in line or "disconnected" in line .lower ()):
199
+ elif process_type == "lsl" and ("Error while closing serial connection" in line or "disconnected" in line .lower ()): # Handle LSL stream interruption
192
200
current_message = "LSL stream error - connection closed"
193
201
lsl_running = False
194
202
stop_dependent_apps ("lsl" )
@@ -197,12 +205,13 @@ def monitor_process_output(process, process_type):
197
205
break
198
206
199
207
def stop_dependent_apps (stream_type ):
208
+ """Stop all dependent applications based on the stream type(LSL/NPG)"""
200
209
global app_processes , current_message , lsl_running , npg_running
201
210
202
- apps_to_stop = []
211
+ apps_to_stop = [] # Track which apps we're stopping for status message
203
212
for app_name , process in list (app_processes .items ()):
204
- if process .poll () is None : # If process is running
205
- apps_to_stop .append (app_name )
213
+ if process .poll () is None : # If process is running
214
+ apps_to_stop .append (app_name ) # Add to stopped apps list
206
215
process .terminate ()
207
216
try :
208
217
process .wait (timeout = 2 )
@@ -225,27 +234,28 @@ def stop_dependent_apps(stream_type):
225
234
226
235
@app .route ("/start_lsl" , methods = ["POST" ])
227
236
def start_lsl ():
237
+ """Start the LSL stream and handle CSV saving option."""
228
238
global lsl_process , lsl_running , current_message
229
239
save_csv = request .form .get ('csv' , 'false' ).lower () == 'true'
230
240
231
- if npg_running :
241
+ if npg_running : # Check for conflicting NPG stream
232
242
current_message = "Please stop NPG stream first"
233
243
return redirect (url_for ('home' ))
234
244
235
- if lsl_running :
245
+ if lsl_running : # Prevent duplicate LSL streams
236
246
current_message = "LSL stream already running"
237
247
return redirect (url_for ('home' ))
238
248
239
249
try :
240
- command = ["python" , "chords.py" , "--lsl" ]
241
- if save_csv :
250
+ command = ["python" , "chords.py" , "--lsl" ] # Command to start chords.py with LSL streaming
251
+ if save_csv : # Check if CSV saving is requested
242
252
command .append ("--csv" )
243
253
244
254
creation_flags = subprocess .CREATE_NO_WINDOW if sys .platform == "win32" else 0
245
255
lsl_process = subprocess .Popen (command , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , creationflags = creation_flags , text = True , bufsize = 1 )
246
256
247
257
output = lsl_process .stdout .readline ().strip ()
248
- if "No" in output :
258
+ if "No" in output : # Check for failure messages in output
249
259
current_message = "Failed to start LSL stream"
250
260
lsl_running = False
251
261
else :
@@ -258,7 +268,7 @@ def start_lsl():
258
268
lsl_process .terminate ()
259
269
return redirect (url_for ('home' ))
260
270
261
- monitor_thread = Thread (target = monitor_process_output , args = (lsl_process , "lsl" ), daemon = True )
271
+ monitor_thread = Thread (target = monitor_process_output , args = (lsl_process , "lsl" ), daemon = True ) # Create a new thread to monitor the LSL process output
262
272
monitor_thread .start ()
263
273
264
274
except Exception as e :
@@ -269,18 +279,19 @@ def start_lsl():
269
279
270
280
@app .route ("/start_npg" , methods = ["POST" ])
271
281
def start_npg ():
282
+ """ Start the NPG stream and handle connection to the selected device."""
272
283
global npg_process , npg_running , current_message
273
284
274
- if lsl_running :
285
+ if lsl_running : # Check for conflicting LSL stream
275
286
current_message = "Please stop LSL stream first"
276
287
return jsonify ({"status" : "error" , "message" : current_message })
277
288
278
289
device_address = session .get ('selected_device' )
279
- if not device_address :
290
+ if not device_address : # Check if a device is selected
280
291
current_message = "No device selected"
281
292
return jsonify ({"status" : "error" , "message" : current_message })
282
293
283
- if npg_running :
294
+ if npg_running : # Prevent duplicate NPG streams
284
295
current_message = "NPG already running"
285
296
return jsonify ({"status" : "error" , "message" : current_message })
286
297
@@ -301,27 +312,28 @@ def start_npg():
301
312
302
313
@app .route ("/run_app" , methods = ["POST" ])
303
314
def run_app ():
315
+ """Start a specified application process."""
304
316
global current_message
305
317
app_name = request .form .get ("app_name" )
306
318
valid_apps = ["heartbeat_ecg" , "emgenvelope" , "eog" , "ffteeg" , "game" , "beetle" , "gui" , "keystroke" , "csvplotter" ]
307
319
308
- if not (lsl_running or npg_running ):
320
+ if not (lsl_running or npg_running ): # Verify either one of the streams is running
309
321
current_message = "Start LSL or NPG first!"
310
322
return redirect (url_for ('home' ))
311
323
312
324
if app_name not in valid_apps :
313
325
current_message = "Invalid application"
314
326
return redirect (url_for ('home' ))
315
327
316
- if app_name in app_processes and app_processes [app_name ].poll () is None :
328
+ if app_name in app_processes and app_processes [app_name ].poll () is None : # Check if application is already running
317
329
current_message = f"{ app_name } is already running"
318
330
return redirect (url_for ('home' ))
319
331
320
332
try :
321
333
creation_flags = subprocess .CREATE_NO_WINDOW if sys .platform == "win32" else 0
322
- process = subprocess .Popen (["python" , f"{ app_name } .py" ], creationflags = creation_flags )
334
+ process = subprocess .Popen (["python" , f"{ app_name } .py" ], creationflags = creation_flags ) # Start the application process
323
335
324
- app_processes [app_name ] = process
336
+ app_processes [app_name ] = process # Track running process in global dictionary
325
337
current_message = f"{ app_name } started successfully"
326
338
except Exception as e :
327
339
current_message = f"Error starting { app_name } : { str (e )} "
@@ -330,11 +342,13 @@ def run_app():
330
342
331
343
@app .route ("/stream_events" )
332
344
def stream_events ():
345
+ """Stream events to the client using Server-Sent Events (SSE)."""
333
346
def event_stream ():
334
- last_state = None
347
+ """Generator function that yields system state updates when changes occur."""
348
+ last_state = None # Initialize last_state to None
335
349
336
350
while True :
337
- current_state = {"lsl_running" : lsl_running , "npg_running" : npg_running , "running_apps" : [k for k ,v in app_processes .items () if v .poll () is None ], "message" : current_message , "stream_interrupted" : ("Data Interrupted" in current_message if current_message else False )}
351
+ current_state = {"lsl_running" : lsl_running , "npg_running" : npg_running , "running_apps" : [k for k ,v in app_processes .items () if v .poll () is None ], "message" : current_message , "stream_interrupted" : (( "Data Interrupted (Bluetooth Disconnected)" or "Error while closing serial connection" in current_message ) if current_message else False )}
338
352
if current_state != last_state :
339
353
yield f"data: { json .dumps (current_state )} \n \n "
340
354
last_state = current_state .copy ()
@@ -345,12 +359,14 @@ def event_stream():
345
359
346
360
@app .route ("/stop_all" , methods = ['POST' ])
347
361
def stop_all ():
362
+ """ Stop all running processes and clear the current message."""
348
363
global current_message
349
364
stop_all_processes ()
350
365
current_message = "All processes stopped"
351
366
return redirect (url_for ('home' ))
352
367
353
368
def cleanup_processes ():
369
+ """ Remove finished processes from the app_processes dictionary."""
354
370
global app_processes
355
371
app_processes = {
356
372
k : v for k , v in app_processes .items ()
@@ -359,10 +375,12 @@ def cleanup_processes():
359
375
360
376
@app .route ("/check_app_status" , methods = ["GET" ])
361
377
def check_app_status ():
378
+ """ Check the status of running applications and return a JSON response."""
362
379
cleanup_processes () # Remove finished processes
363
380
return jsonify ({"running_apps" : list (app_processes .keys ())})
364
381
365
382
def stop_all_processes ():
383
+ """Stop all running processes and clear the current message."""
366
384
global lsl_process , npg_process , app_processes , lsl_running , npg_running , current_message
367
385
368
386
# Terminate LSL process
@@ -373,24 +391,23 @@ def stop_all_processes():
373
391
except subprocess .TimeoutExpired :
374
392
lsl_process .kill ()
375
393
lsl_running = False
394
+ stop_dependent_apps ("lsl" )
376
395
377
396
if npg_process and npg_process .poll () is None :
378
- npg_process .send_signal ( signal . SIGINT )
397
+ npg_process .terminate ( )
379
398
try :
380
399
npg_process .wait (timeout = 3 )
381
400
except subprocess .TimeoutExpired :
382
- npg_process .terminate ()
383
- try :
384
- npg_process .wait (timeout = 2 )
385
- except subprocess .TimeoutExpired :
386
- npg_process .kill ()
401
+ npg_process .kill ()
387
402
npg_running = False
403
+ stop_dependent_apps ("npg" )
388
404
389
405
stop_dependent_apps ("all" )
390
406
current_message = "All processes stopped"
391
407
print ("All processes terminated." )
392
408
393
409
def handle_sigint (signal_num , frame ):
410
+ """Handle Ctrl+C signal to stop all processes gracefully."""
394
411
print ("\n Ctrl+C pressed! Stopping all processes..." )
395
412
stop_all_processes ()
396
413
sys .exit (0 )
0 commit comments