Skip to content

Commit 6125a44

Browse files
committed
Added functionality for filters and creating separate .yaml file for app list
1 parent 8b7a214 commit 6125a44

File tree

4 files changed

+266
-45
lines changed

4 files changed

+266
-45
lines changed

app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from flask import Flask, render_template, request, jsonify
1+
from flask import Flask, render_template, request, jsonify, send_from_directory
22
from connection import Connection
33
import threading
44
import asyncio
55
import logging
66
from bleak import BleakScanner
77
from flask import Response
88
import queue
9+
import yaml
10+
from pathlib import Path
911

1012
console_queue = queue.Queue()
1113
app = Flask(__name__)
@@ -32,6 +34,24 @@ def wrapper(*args, **kwargs):
3234
def index():
3335
return render_template('index.html')
3436

37+
@app.route('/get_apps_config')
38+
def get_apps_config():
39+
try:
40+
# Try to load from config/apps.yaml first
41+
config_path = Path('config') / 'apps.yaml'
42+
if config_path.exists():
43+
with open(config_path, 'r') as file:
44+
config = yaml.safe_load(file)
45+
return jsonify(config)
46+
47+
# Fallback to built-in apps if YAML doesn't exist
48+
return jsonify
49+
50+
except Exception as e:
51+
logging.error(f"Error loading apps config: {str(e)}")
52+
# Minimal fallback if everything fails
53+
return jsonify
54+
3555
@app.route('/scan_ble')
3656
@run_async
3757
async def scan_ble_devices():

config/apps.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
apps:
2+
- title: "ECG with Heart Rate"
3+
icon: "fa-heartbeat"
4+
color: "cyan"
5+
script: "heartbeat_ecg"
6+
description: "Visualize electrocardiogram (ECG) data with real-time BPM calculation."
7+
category: "ECG"
8+
9+
- title: "EMG with Envelope"
10+
icon: "fa-wave-square"
11+
color: "red"
12+
script: "emgenvelope"
13+
description: "Monitor electromyography (EMG) signals with envelope detection for muscle activity."
14+
category: "EMG"
15+
16+
- title: "EOG with Blinks"
17+
icon: "fa-eye"
18+
color: "purple"
19+
script: "eog"
20+
description: "Track electrooculography (EOG) signals to detect eye blinks."
21+
category: "EOG"
22+
23+
- title: "EEG with FFT"
24+
icon: "fa-brain"
25+
color: "green"
26+
script: "ffteeg"
27+
description: "Analyze electroencephalography (EEG) data with FFT visualization and Brain Power Bands."
28+
category: "EEG"
29+
30+
- title: "EEG Tug of War game"
31+
icon: "fa-gamepad"
32+
color: "yellow"
33+
script: "game"
34+
description: "Play a tug-of-war game using your betawaves (EEG signals)."
35+
category: "EEG"
36+
37+
- title: "EEG Beetle Game"
38+
icon: "fa-bug"
39+
color: "pink"
40+
script: "beetle"
41+
description: "Play a beetle game controlled by your EEG brainwave patterns."
42+
category: "EEG"
43+
44+
- title: "EOG Keystroke Emulator"
45+
icon: "fa-keyboard"
46+
color: "blue"
47+
script: "keystroke"
48+
description: "Use eye movements (EOG) to emulate keyboard keystrokes for hands-free control."
49+
category: "EOG"
50+
51+
- title: "GUI Visualization"
52+
icon: "fa-chart-bar"
53+
color: "orange"
54+
script: "gui"
55+
description: "GUI for visualizing various biopotential signals."
56+
category: "Tools"
57+
58+
- title: "CSV Plotter"
59+
icon: "fa-chart-line"
60+
color: "teal"
61+
script: "csvplotter"
62+
description: "Load and plot data from CSV files for offline analysis."
63+
category: "Tools"

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ tk==0.1.0
1515
PyAutoGUI==0.9.54
1616
Flask==3.1.0
1717
psutil==6.1.1
18-
websocket-client==1.8.0
18+
websocket-client==1.8.0
19+
PyYAML==6.0.2

templates/index.html

Lines changed: 180 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -141,57 +141,190 @@ <h2 class="text-xl font-semibold text-gray-700 dark:text-gray-200">Applications<
141141
}
142142
</script>
143143
<script>
144-
const apps = [
145-
{ title: 'ECG with Heart Rate', icon: 'fa-heartbeat', color: 'cyan', script: 'heartbeat_ecg' },
146-
{ title: 'EMG with Envelope', icon: 'fa-wave-square', color: 'red', script: 'emgenvelope' },
147-
{ title: 'EOG with Blinks', icon: 'fa-eye', color: 'purple', script: 'eog' },
148-
{ title: 'EEG with FFT', icon: 'fa-brain', color: 'green', script: 'ffteeg' },
149-
{ title: 'EEG Tug of War game', icon: 'fa-gamepad', color: 'yellow', script: 'game' },
150-
{ title: 'EEG Beetle Game', icon: 'fa-bug', color: 'pink', script: 'beetle' },
151-
{ title: 'EOG Keystroke Emulator', icon: 'fa-keyboard', color: 'blue', script: 'keystroke' },
152-
{ title: 'GUI Visualization', icon: 'fa-chart-bar', color: 'orange', script: 'gui' },
153-
{ title: 'CSV Plotter', icon: 'fa-chart-line', color: 'teal', script: 'csvplotter' },
154-
];
155-
156-
const appGrid = document.getElementById('app-grid');
157-
apps.forEach(app => {
158-
const card = document.createElement('div');
159-
card.className = 'bg-gradient-to-b from-white to-gray-50 dark:from-gray-700 dark:to-gray-800 rounded-xl shadow border hover:shadow-lg transition duration-300 dark:border-gray-700 cursor-pointer';
160-
card.innerHTML = `
161-
<div class="p-4">
162-
<div class="w-full h-32 bg-${app.color}-100 dark:bg-${app.color}-900 rounded-lg flex items-center justify-center mb-4">
163-
<i class="fas ${app.icon} text-4xl text-${app.color}-600 dark:text-${app.color}-300"></i>
144+
async function loadApps() {
145+
try {
146+
const appGrid = document.getElementById('app-grid');
147+
appGrid.innerHTML = `
148+
<div class="col-span-full flex flex-col items-center justify-center py-12">
149+
<i class="fas fa-circle-notch fa-spin text-cyan-500 text-4xl mb-4"></i>
150+
<p class="text-gray-600 dark:text-gray-400">Loading applications...</p>
164151
</div>
165-
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-1">${app.title}</h3>
166-
<p class="text-sm text-gray-600 dark:text-gray-400">Click to launch ${app.title}</p>
167-
</div>
168-
`;
169-
170-
card.addEventListener('click', async () => {
171-
if (!isConnected) {
172-
showAlert('Please start LSL Stream first by any protocol (USB/WIFI/BLE)');
173-
return;
152+
`;
153+
const response = await fetch('/get_apps_config');
154+
155+
if (!response.ok) {
156+
throw new Error(`HTTP error! status: ${response.status}`);
157+
}
158+
159+
const config = await response.json();
160+
161+
if (!config?.apps || !Array.isArray(config.apps)) {
162+
throw new Error('Invalid apps configuration format');
174163
}
164+
165+
return config.apps;
166+
} catch (error) {
167+
console.error('Error loading apps config:', error);
175168

176-
// First checking if app is already running
177-
try {
178-
const response = await fetch(`/check_app_status/${app.script}`);
179-
const data = await response.json();
180-
181-
if (data.status === 'running') {
182-
showAlert(`${app.title} is already running!`);
169+
// Show error state to user
170+
const appGrid = document.getElementById('app-grid');
171+
appGrid.innerHTML = `
172+
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center">
173+
<i class="fas fa-exclamation-triangle text-red-500 text-4xl mb-4"></i>
174+
<h3 class="text-lg font-medium text-gray-800 dark:text-gray-200 mb-2">
175+
Failed to load applications
176+
</h3>
177+
<p class="text-gray-600 dark:text-gray-400 max-w-md">
178+
Could not load application configuration. Please try refreshing the page.
179+
</p>
180+
<button onclick="window.location.reload()" class="mt-4 px-4 py-2 bg-cyan-500 text-white rounded-lg hover:bg-cyan-600 transition-colors">
181+
<i class="fas fa-sync-alt mr-2"></i> Refresh
182+
</button>
183+
</div>
184+
`;
185+
}
186+
}
187+
188+
// Initialize the application with proper error handling
189+
async function initializeApplication() {
190+
try {
191+
const apps = await loadApps();
192+
renderApps(apps);
193+
setupCategoryFilter(apps);
194+
} catch (error) {
195+
console.error('Application initialization failed:', error);
196+
}
197+
}
198+
199+
// Render apps to the grid with improved card design
200+
function renderApps(apps) {
201+
const appGrid = document.getElementById('app-grid');
202+
203+
if (!apps || apps.length === 0) {
204+
appGrid.innerHTML = `
205+
<div class="col-span-full flex flex-col items-center justify-center py-12">
206+
<i class="fas fa-folder-open text-gray-400 text-4xl mb-4"></i>
207+
<p class="text-gray-600 dark:text-gray-400">No applications available</p>
208+
</div>
209+
`;
210+
return;
211+
}
212+
213+
appGrid.innerHTML = '';
214+
215+
apps.forEach(app => {
216+
const card = document.createElement('div');
217+
card.className = `group bg-gradient-to-b from-white to-gray-50 dark:from-gray-700 dark:to-gray-800 rounded-xl shadow border hover:shadow-lg transition-all duration-300 dark:border-gray-700 cursor-pointer overflow-hidden`;
218+
219+
card.innerHTML = `
220+
<div class="relative h-full flex flex-col">
221+
<div class="w-full h-32 bg-${app.color}-100 dark:bg-${app.color}-900 rounded-t-lg flex items-center justify-center transition-all duration-300 group-hover:opacity-90">
222+
<i class="fas ${app.icon} text-4xl text-${app.color}-600 dark:text-${app.color}-300"></i>
223+
</div>
224+
<div class="p-4 flex-1 flex flex-col">
225+
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-1">${app.title}</h3>
226+
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 flex-1">${app.description}</p>
227+
</div>
228+
</div>
229+
`;
230+
231+
card.addEventListener('click', async () => {
232+
if (!isConnected) {
233+
showAlert('Please connect to a device first using USB, WiFi or Bluetooth');
183234
return;
184235
}
185-
} catch (error) {
186-
console.error('Error checking app status:', error);
236+
237+
// Add loading state to the clicked card
238+
const originalContent = card.innerHTML;
239+
card.innerHTML = `
240+
<div class="h-full flex items-center justify-center p-4">
241+
<i class="fas fa-circle-notch fa-spin text-${app.color}-500 text-xl mr-2"></i>
242+
<span>Launching ${app.title}...</span>
243+
</div>
244+
`;
245+
246+
try {
247+
const response = await fetch(`/check_app_status/${app.script}`);
248+
249+
if (!response.ok) {
250+
throw new Error('Failed to check app status');
251+
}
252+
253+
const data = await response.json();
254+
255+
if (data.status === 'running') {
256+
showAlert(`${app.title} is already running!`);
257+
card.innerHTML = originalContent;
258+
return;
259+
}
260+
261+
await launchApplication(app.script);
262+
card.innerHTML = originalContent;
263+
} catch (error) {
264+
console.error('Error launching app:', error);
265+
showAlert(`Failed to launch ${app.title}: ${error.message}`);
266+
card.innerHTML = originalContent;
267+
}
268+
});
269+
270+
appGrid.appendChild(card);
271+
});
272+
}
273+
274+
// Set up category filter with fixed options
275+
function setupCategoryFilter(apps) {
276+
const categorySelect = document.querySelector('select');
277+
278+
const fixedCategories = ['All', 'ECG', 'EMG', 'EOG', 'EEG', 'Tools']; // Fixed filter options
279+
categorySelect.innerHTML = ''; // Clear existing options
280+
281+
// Add fixed options
282+
fixedCategories.forEach(category => {
283+
const option = document.createElement('option');
284+
option.value = category;
285+
option.textContent = category;
286+
287+
// Disable option if no apps exist for this category (except 'All')
288+
if (category !== 'All') {
289+
const hasApps = apps.some(app => app.category === category);
290+
option.disabled = !hasApps;
291+
if (!hasApps) {
292+
option.textContent += ' (0)';
293+
}
187294
}
188295

189-
// If not running, launch the app
190-
launchApplication(app.script);
296+
categorySelect.appendChild(option);
297+
});
298+
299+
// Add event listener for filtering
300+
categorySelect.addEventListener('change', (e) => {
301+
const selectedCategory = e.target.value;
302+
filterAppsByCategory(selectedCategory, apps);
191303
});
304+
}
305+
306+
// Filter apps by category with smooth transition
307+
function filterAppsByCategory(category, allApps) {
308+
const appGrid = document.getElementById('app-grid');
192309

193-
appGrid.appendChild(card);
194-
});
310+
// Add fade-out effect
311+
appGrid.style.opacity = '0.5';
312+
appGrid.style.transition = 'opacity 0.3s ease';
313+
314+
setTimeout(() => {
315+
const filteredApps = category === 'All' ?
316+
allApps :
317+
allApps.filter(app => app.category === category);
318+
319+
renderApps(filteredApps);
320+
321+
// Add fade-in effect
322+
appGrid.style.opacity = '0';
323+
setTimeout(() => {
324+
appGrid.style.opacity = '1';
325+
}, 10);
326+
}, 300);
327+
}
195328

196329
// Theme toggle
197330
const themeToggle = document.getElementById('theme-toggle');
@@ -270,7 +403,6 @@ <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-1">${app.ti
270403
}
271404
} else {
272405
btn.classList.remove('cursor-not-allowed');
273-
// Restore hover effects
274406
btn.classList.add('hover:bg-cyan-500', 'hover:text-white');
275407
btn.classList.remove('bg-cyan-600', 'dark:bg-cyan-700');
276408
}
@@ -769,13 +901,18 @@ <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-1">${app.ti
769901
// Call it initially when the page loads
770902
checkStreamStatus();
771903

904+
// Initialize the app when DOM is loaded
905+
document.addEventListener('DOMContentLoaded', () => {
906+
initializeApplication();
907+
772908
document.getElementById('github-btn').addEventListener('click', () => {
773909
window.open('https://github.com/upsidedownlabs/Chords-Python', '_blank');
774910
});
775911

776912
document.getElementById('info-btn').addEventListener('click', () => {
777913
alert('Chords Python - Biopotential Data Acquisition System\nVersion 2.1.0');
778914
});
915+
});
779916
</script>
780917
</body>
781918
</html>

0 commit comments

Comments
 (0)