Explorar o código

improve websocket implementation

Tuan Nguyen hai 11 meses
pai
achega
f343c97975
Modificáronse 4 ficheiros con 129 adicións e 110 borrados
  1. 27 21
      app.py
  2. 11 5
      modules/core/pattern_manager.py
  3. 24 3
      static/css/style.css
  4. 67 81
      static/js/main.js

+ 27 - 21
app.py

@@ -92,34 +92,44 @@ active_status_connections = set()
 
 @app.websocket("/ws/status")
 async def websocket_status_endpoint(websocket: WebSocket):
-    client = websocket.client
-    logger.info(f"New WebSocket connection request from {client.host}:{client.port}")
-    
     await websocket.accept()
     active_status_connections.add(websocket)
-    
     try:
         while True:
-            # Send status updates every second
             status = pattern_manager.get_status()
-            await websocket.send_json(status)
-            await asyncio.sleep(1)  # Wait for 1 second before sending next update
+            try:
+                await websocket.send_json({
+                    "type": "status_update",
+                    "data": status
+                })
+            except RuntimeError as e:
+                if "close message has been sent" in str(e):
+                    break
+                raise
+            await asyncio.sleep(1)
     except WebSocketDisconnect:
-        active_status_connections.remove(websocket)
-        logger.info(f"Client disconnected: {client.host}:{client.port}")
+        pass
+    finally:
+        active_status_connections.discard(websocket)
+        try:
+            await websocket.close()
+        except RuntimeError:
+            pass
 
 async def broadcast_status_update(status: dict):
     """Broadcast status update to all connected clients."""
     disconnected = set()
     for websocket in active_status_connections:
         try:
-            await websocket.send_json(status)
-            logger.debug(f"Status broadcast to client {websocket.client.host}:{websocket.client.port}")
+            await websocket.send_json({
+                "type": "status_update",
+                "data": status
+            })
         except WebSocketDisconnect:
             disconnected.add(websocket)
-            logger.debug(f"Client disconnected during broadcast: {websocket.client.host}:{websocket.client.port}")
+        except RuntimeError:
+            disconnected.add(websocket)
     
-    # Clean up disconnected clients
     active_status_connections.difference_update(disconnected)
 
 # FastAPI routes
@@ -222,10 +232,12 @@ async def run_theta_rho(request: ThetaRhoRequest, background_tasks: BackgroundTa
             
         files_to_run = [file_path]
         logger.info(f'Running theta-rho file: {request.file_name} with pre_execution={request.pre_execution}')
+        
+        # Pass arguments in the correct order
         background_tasks.add_task(
             pattern_manager.run_theta_rho_files,
-            files_to_run,
-            clear_pattern=request.pre_execution if request.pre_execution != "none" else None
+            files_to_run,  # First positional argument
+            clear_pattern=request.pre_execution if request.pre_execution != "none" else None  # Named argument
         )
         return {"success": True}
     except Exception as e:
@@ -370,12 +382,6 @@ async def pause_execution():
         return {"success": True, "message": "Execution paused"}
     raise HTTPException(status_code=500, detail="Failed to pause execution")
 
-@app.get("/status")
-async def get_status():
-    status = pattern_manager.get_status()
-    await broadcast_status_update(status)
-    return status
-
 @app.post("/resume_execution")
 async def resume_execution():
     if pattern_manager.resume_execution():

+ 11 - 5
modules/core/pattern_manager.py

@@ -155,15 +155,21 @@ async def run_theta_rho_file(file_path):
 
                 move_polar(theta, rho)
                 
-                if i != 0:
-                    pbar.update(1)
-                    elapsed_time = time.time() - start_time
-                    estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
-                    state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
+                # Update progress for all coordinates including the first one
+                pbar.update(1)
+                elapsed_time = time.time() - start_time
+                estimated_remaining_time = (total_coordinates - (i + 1)) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
+                state.execution_progress = (i + 1, total_coordinates, estimated_remaining_time, elapsed_time)
                 
                 # Add a small delay to allow other async operations
                 await asyncio.sleep(0.001)
 
+        # Update progress one last time to show 100%
+        elapsed_time = time.time() - start_time
+        state.execution_progress = (total_coordinates, total_coordinates, 0, elapsed_time)
+        # Give WebSocket a chance to send the final update
+        await asyncio.sleep(0.1)
+        
         connection_manager.check_idle()
         state.current_playing_file = None
         state.execution_progress = None

+ 24 - 3
static/css/style.css

@@ -1022,8 +1022,16 @@ body.playing #currently-playing-container:not(.open) #progress-container {
     background-color: var(--border-primary);
     border-radius: 4px;
     overflow: hidden;
+    margin-right: 20px !important;  /* Add forced margin to ensure spacing */
 }
 
+#play_progress_text {
+    font-size: 0.9rem;
+    min-width: 45px;  /* Ensure consistent spacing for different percentages */
+    flex-shrink: 0;   /* Prevent text from shrinking */
+}
+
+/* Progress Bar Styles */
 #play_progress::-webkit-progress-bar {
     background-color: var(--border-primary);
 }
@@ -1033,9 +1041,22 @@ body.playing #currently-playing-container:not(.open) #progress-container {
     transition: width 0.25s ease;
 }
 
-#play_progress_text {
-    font-size: 0.9rem;
-    margin-left: 10px;
+/* Add dark mode specific styles */
+:root[data-theme="dark"] #play_progress {
+    background-color: var(--background-tertiary-dark);
+}
+
+:root[data-theme="dark"] #play_progress::-webkit-progress-bar {
+    background-color: var(--background-tertiary-dark);
+}
+
+:root[data-theme="dark"] #play_progress::-webkit-progress-value {
+    background-color: var(--color-success);  /* Using success color for better visibility */
+}
+
+/* Progress text color in dark mode */
+:root[data-theme="dark"] #play_progress_text {
+    color: var(--text-secondary-dark);
 }
 
 .play-buttons {

+ 67 - 81
static/js/main.js

@@ -260,8 +260,15 @@ async function runThetaRho() {
 
         const result = await response.json();
         if (response.ok) {
+            // Show the currently playing UI immediately
+            document.body.classList.add('playing');
+            const currentlyPlayingFile = document.getElementById('currently-playing-file');
+            if (currentlyPlayingFile) {
+                currentlyPlayingFile.textContent = selectedFile.replace('./patterns/', '');
+            }
+            // Show initial preview
+            previewPattern(selectedFile.replace('./patterns/', ''), 'currently-playing-container');
             logMessage(`Pattern running: ${selectedFile}`, LOG_TYPE.SUCCESS);
-            updateCurrentlyPlaying();
         } else {
             if (response.status === 409) {
                 logMessage("Cannot start pattern: Another pattern is already running", LOG_TYPE.WARNING);
@@ -272,7 +279,6 @@ async function runThetaRho() {
     } catch (error) {
         logMessage(`Error running pattern: ${error.message}`, LOG_TYPE.ERROR);
     }
-    updateCurrentlyPlaying();
 }
 
 async function stopExecution() {
@@ -1430,71 +1436,7 @@ function attachFullScreenListeners() {
 
 let lastPreviewedFile = null; // Track the last previewed file
 
-async function updateCurrentlyPlaying() {
-    try {
-        if (!document.hasFocus()) return; // Stop execution if the page is not visible
-
-        const response = await fetch('/status');
-        const data = await response.json();
-
-        const currentlyPlayingSection = document.getElementById('currently-playing-container');
-        if (!currentlyPlayingSection) {
-            logMessage('Currently Playing section not found.', LOG_TYPE.ERROR);
-            return;
-        }
-
-        const currentSpeedElem = document.getElementById('current_speed_display');
-        if (data.current_speed !== undefined) {
-            currentSpeedElem.textContent = `Current Speed: ${data.current_speed}`;
-        } else {
-            currentSpeedElem.textContent = 'Current Speed: N/A';
-        }
-
-        if (data.current_playing_file && !data.stop_requested) {
-            const { current_playing_file, execution_progress, pause_requested } = data;
 
-            // Strip './patterns/' prefix from the file name
-            const fileName = current_playing_file.replace('./patterns/', '');
-
-            if (!document.body.classList.contains('playing')) {
-                closeStickySection('pattern-preview-container')
-            }
-
-            // Show "Currently Playing" section
-            document.body.classList.add('playing');
-
-            // Update pattern preview only if the file is different
-            if (current_playing_file !== lastPreviewedFile) {
-                previewPattern(fileName, 'currently-playing-container');
-                lastPreviewedFile = current_playing_file;
-            }
-
-            // Update the filename display
-            const fileNameDisplay = document.getElementById('currently-playing-file');
-            if (fileNameDisplay) fileNameDisplay.textContent = fileName;
-
-            // Update progress bar
-            const progressBar = document.getElementById('play_progress');
-            const progressText = document.getElementById('play_progress_text');
-            if (execution_progress) {
-                const progressPercentage = (execution_progress[0] / execution_progress[1]) * 100;
-                progressBar.value = progressPercentage;
-                progressText.textContent = `${Math.round(progressPercentage)}% (Elapsed: ${formatSecondsToHMS(execution_progress[3])} | Remaining: ${formatSecondsToHMS(execution_progress[2])})`;
-            } else {
-                progressBar.value = 0;
-                progressText.textContent = '0%';
-            }
-
-            // Update play/pause button
-            const pausePlayButton = document.getElementById('pausePlayCurrent');
-            if (pausePlayButton) pausePlayButton.innerHTML = pause_requested ? '<i class="fa-solid fa-play"></i>' : '<i class="fa-solid fa-pause"></i>';
-        } else {
-            document.body.classList.remove('playing');
-        }
-    } catch (error) {
-        logMessage(`Error updating "Currently Playing" section: ${error.message}`);
-    }
-}
 
 function formatSecondsToHMS(seconds) {
     const hrs = Math.floor(seconds / 3600);
@@ -1504,12 +1446,6 @@ function formatSecondsToHMS(seconds) {
 }
 
 // Function to start or stop updates based on visibility
-function handleVisibilityChange() {
-    if (document.hasFocus()) {
-        updateCurrentlyPlaying(); // Run immediately
-    }
-}
-
 function toggleSettings() {
     const settingsContainer = document.getElementById('settings-container');
     if (settingsContainer) {
@@ -1757,11 +1693,13 @@ const savedTheme = localStorage.getItem('theme') || 'light';
 document.documentElement.setAttribute('data-theme', savedTheme);
 themeIcon.className = savedTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
 
-document.addEventListener("visibilitychange", handleVisibilityChange);
 
 // Add WebSocket connection for status updates
 let statusSocket = null;
 
+let reconnectAttempts = 0;
+const MAX_RECONNECT_ATTEMPTS = 5;
+
 function connectStatusWebSocket() {
     // Close existing connection if any
     if (statusSocket) {
@@ -1774,18 +1712,44 @@ function connectStatusWebSocket() {
 
     statusSocket.onopen = () => {
         console.log('Status WebSocket connected');
+        reconnectAttempts = 0; // Reset reconnect attempts on successful connection
         // Request initial status
         statusSocket.send('get_status');
+        
+        // Request status updates periodically
+        setInterval(() => {
+            if (statusSocket.readyState === WebSocket.OPEN) {
+                statusSocket.send('get_status');
+            }
+        }, 1000); // Request update every second
     };
 
+    // Add debouncing for UI updates
+    let updateTimeout;
     statusSocket.onmessage = (event) => {
-        const status = JSON.parse(event.data);
-        updateCurrentlyPlayingUI(status);
+        try {
+            const status = JSON.parse(event.data);
+            console.log('Received status update:', status);
+            clearTimeout(updateTimeout);
+            updateTimeout = setTimeout(() => {
+                console.log('Updating UI with status:', status);
+                updateCurrentlyPlayingUI(status);
+            }, 100); // Debounce updates to 100ms
+        } catch (error) {
+            console.error('Error processing status update:', error);
+        }
     };
 
     statusSocket.onclose = () => {
-        console.log('Status WebSocket disconnected. Reconnecting in 5 seconds...');
-        setTimeout(connectStatusWebSocket, 5000);
+        console.log('Status WebSocket disconnected.');
+        if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+            reconnectAttempts++;
+            const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); // Exponential backoff, max 30 seconds
+            console.log(`Attempting to reconnect in ${delay/1000} seconds... (Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
+            setTimeout(connectStatusWebSocket, delay);
+        } else {
+            console.error('Max reconnection attempts reached. Please refresh the page.');
+        }
     };
 
     statusSocket.onerror = (error) => {
@@ -1819,7 +1783,14 @@ document.addEventListener('DOMContentLoaded', () => {
     checkForUpdates();
 });
 
+// Track the last time we had a file playing
+let lastPlayingTime = 0;
+const HIDE_DELAY = 5000; // 1 second delay before hiding
+
 // Update the updateCurrentlyPlayingUI function to handle WebSocket updates
+// Track the last played file to detect when a new pattern starts
+let lastPlayedFile = null;
+
 function updateCurrentlyPlayingUI(status) {
     const currentlyPlayingContainer = document.getElementById('currently-playing-container');
     const currentlyPlayingFile = document.getElementById('currently-playing-file');
@@ -1829,16 +1800,31 @@ function updateCurrentlyPlayingUI(status) {
 
     if (!currentlyPlayingContainer || !currentlyPlayingFile || !progressBar || !progressText) return;
 
+    const now = Date.now();
+    
     if (status.current_file) {
+        // Update last playing time
+        lastPlayingTime = now;
+        
         // Show the container and update file name
         document.body.classList.add('playing');
-        currentlyPlayingFile.textContent = status.current_file.replace('./patterns/', '');
+        const fileName = status.current_file.replace('./patterns/', '');
+        currentlyPlayingFile.textContent = fileName;
+        
+        // Only update preview when a new pattern starts playing
+        if (lastPlayedFile !== status.current_file) {
+            lastPlayedFile = status.current_file;
+            // Remove the ./patterns/ prefix since previewPattern will add it
+            const cleanFileName = status.current_file.replace('./patterns/', '');
+            // Use the existing preview_thr endpoint
+            previewPattern(cleanFileName, 'currently-playing-container');
+        }
         
         // Update progress information if available
         if (status.progress) {
             const progress = status.progress;
             const percentage = progress.percentage.toFixed(1);
-            const remainingTime = progress.remaining_time ? formatSecondsToHMS(progress.remaining_time) : 'calculating...';
+            const remainingTime = progress.remaining_time === null ? 'calculating...' : formatSecondsToHMS(progress.remaining_time);
             const elapsedTime = formatSecondsToHMS(progress.elapsed_time);
             
             progressBar.value = percentage;
@@ -1854,8 +1840,8 @@ function updateCurrentlyPlayingUI(status) {
                 '<i class="fa-solid fa-play"></i>' : 
                 '<i class="fa-solid fa-pause"></i>';
         }
-    } else {
-        // Hide the container when no file is playing
+    } else if (now - lastPlayingTime > HIDE_DELAY) {
+        // Only hide the container if we haven't had a playing file for more than HIDE_DELAY
         document.body.classList.remove('playing');
         progressBar.value = 0;
         progressText.textContent = '0%';