Tuan Nguyen vor 11 Monaten
Ursprung
Commit
7db640c360
5 geänderte Dateien mit 223 neuen und 147 gelöschten Zeilen
  1. 51 13
      app.py
  2. 75 37
      modules/core/pattern_manager.py
  3. 58 97
      static/js/main.js
  4. 4 0
      templates/index.html
  5. 35 0
      test_websocket.py

+ 51 - 13
app.py

@@ -3,6 +3,10 @@ import atexit
 import os
 import logging
 from datetime import datetime
+import asyncio
+import json
+import threading
+import time
 from modules.connection import connection_manager
 from modules.core import pattern_manager
 from modules.core import playlist_manager
@@ -26,6 +30,10 @@ logger = logging.getLogger(__name__)
 
 app = Flask(__name__)
 
+# Create a global lock and thread tracking variable
+pattern_execution_lock = threading.Lock()
+current_execution_thread = None
+
 # Flask API Endpoints
 @app.route('/')
 def index():
@@ -105,6 +113,8 @@ def upload_theta_rho():
 def run_theta_rho():
     file_name = request.json.get('file_name')
     pre_execution = request.json.get('pre_execution')
+    
+    global current_execution_thread
 
     if not file_name:
         logger.warning('Run theta-rho request received without file name')
@@ -115,19 +125,35 @@ def run_theta_rho():
         logger.error(f'Theta-rho file not found: {file_path}')
         return jsonify({'error': 'File not found'}), 404
 
+    # Check if a pattern is already running
+    if current_execution_thread and current_execution_thread.is_alive():
+        logger.warning(f'Attempted to run pattern while another is already running')
+        return jsonify({'error': 'A pattern is already running. Stop it before starting a new one.'}), 409
+        
     try:
-            
-        if not (state.conn.is_connected() if state.conn else False):
-            logger.warning("Attempted to run a pattern without a connection")
-            return jsonify({"success": False, "error": "Connection not established"}), 400
-        files_to_run = [file_path]
-        logger.info(f'Running theta-rho file: {file_name} with pre_execution={pre_execution}')
-        pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
-        return jsonify({'success': True})
+        # Create a thread that will execute the pattern
+        current_execution_thread = threading.Thread(
+            target=execute_pattern,
+            args=(file_name, pre_execution),
+            daemon=True
+        )
+        
+        # Start the thread
+        current_execution_thread.start()
+        
+        return jsonify({"success": True, "message": f"Pattern {file_name} started in background"})
     except Exception as e:
         logger.error(f'Failed to run theta-rho file {file_name}: {str(e)}')
         return jsonify({'error': str(e)}), 500
 
+def execute_pattern(file_name, pre_execution):
+    if not (state.conn.is_connected() if state.conn else False):
+        logger.warning("Attempted to run a pattern without a connection")
+        return jsonify({"success": False, "error": "Connection not established"}), 400
+    files_to_run = [os.path.join(pattern_manager.THETA_RHO_DIR, file_name)]
+    logger.info(f'Running theta-rho file: {file_name} with pre_execution={pre_execution}')
+    pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
+
 @app.route('/stop_execution', methods=['POST'])
 def stop_execution():
     if not (state.conn.is_connected() if state.conn else False):
@@ -273,6 +299,7 @@ def pause_execution():
 
 @app.route('/status', methods=['GET'])
 def get_status():
+    """Endpoint to get current status information."""
     return jsonify(pattern_manager.get_status())
 
 @app.route('/resume_execution', methods=['POST'])
@@ -437,6 +464,8 @@ def set_wled_ip():
     # Save to state
     state.wled_ip = wled_ip
     state.save()
+    if state.wled_ip:
+        state.led_controlller = LEDController(state.wled_ip)
     logger.info(f"WLED IP updated: {wled_ip}")
 
     return jsonify({"success": True, "wled_ip": state.wled_ip})
@@ -444,11 +473,16 @@ def set_wled_ip():
 
 @app.route('/get_wled_ip', methods=['GET'])
 def get_wled_ip():
-    """Retrieve the saved WLED IP address"""
-    if not state.wled_ip:
-        return jsonify({"success": False, "error": "No WLED IP set"}), 404
-
-    return jsonify({"success": True, "wled_ip": state.wled_ip})
+    # Logic to get WLED IP address
+    try:
+        # Replace with your actual logic to get the WLED IP
+        wled_ip = state.wled_ip if hasattr(state, 'wled_ip') else None
+        if wled_ip:
+            state.led_controlller = LEDController(state.wled_ip)
+        return jsonify({"ip": wled_ip})
+    except Exception as e:
+        logger.error(f"Error getting WLED IP: {str(e)}")
+        return jsonify({"error": str(e)}), 500
 
 @app.route('/skip_pattern', methods=['POST'])
 def skip_pattern():
@@ -457,12 +491,15 @@ def skip_pattern():
     state.skip_requested = True
     return jsonify({"success": True})
 
+
 def on_exit():
     """Function to execute on application shutdown."""
     logger.info("Shutting down gracefully, please wait for execution to complete")
+    
     pattern_manager.stop_actions()
     state.save()
     mqtt.cleanup_mqtt()
+    logger.info("Shutdown complete")
 
 def entrypoint():
     logger.info("Starting Dune Weaver application...")
@@ -482,6 +519,7 @@ def entrypoint():
 
     try:
         logger.info("Starting Flask server on port 8080...")
+        # Run the Flask app
         app.run(debug=False, host='0.0.0.0', port=8080)
     except KeyboardInterrupt:
         logger.info("Keyboard interrupt received. Shutting down.")

+ 75 - 37
modules/core/pattern_manager.py

@@ -17,6 +17,10 @@ logger = logging.getLogger(__name__)
 THETA_RHO_DIR = './patterns'
 os.makedirs(THETA_RHO_DIR, exist_ok=True)
 
+# Threading events
+pause_event = threading.Event()
+pause_event.set()  # Initially not paused
+
 def list_theta_rho_files():
     files = []
     for root, _, filenames in os.walk(THETA_RHO_DIR):
@@ -169,6 +173,7 @@ def pause_execution():
     logger.info("Pausing pattern execution")
     with state.pause_condition:
         state.pause_requested = True
+    pause_event.clear()  # Clear event to block execution
     return True
 
 def resume_execution():
@@ -176,6 +181,7 @@ def resume_execution():
     with state.pause_condition:
         state.pause_requested = False
         state.pause_condition.notify_all()
+    pause_event.set()  # Set event to allow execution to continue
     return True
     
 def reset_theta():
@@ -200,6 +206,7 @@ def run_theta_rho_file(file_path):
     # if not file_path:
     #     return
     
+    state.current_playing_file = file_path
     coordinates = parse_theta_rho_file(file_path)
     total_coordinates = len(coordinates)
 
@@ -209,21 +216,20 @@ def run_theta_rho_file(file_path):
         state.execution_progress = None
         return
 
-    state.execution_progress = (0, total_coordinates, None)
-    
-    # stop actions without resetting the playlist
-    stop_actions(clear_playlist=False)
 
-    state.current_playing_file = file_path
-    state.execution_progress = (0, 0, None)
+    # stop actions without resetting the playlist
+    state.execution_progress = (0, total_coordinates, None, 0)
     state.stop_requested = False
     logger.info(f"Starting pattern execution: {file_path}")
     logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
     reset_theta()
     
     if state.led_controller:
-            effect_playing(state.led_controller)
-            
+        effect_playing(state.led_controller)
+    
+    # Track last status update time for time-based updates
+    last_status_update = time.time()
+    status_update_interval = 0.5  # Update status every 0.5 seconds
     
     with tqdm(
         total=total_coordinates,
@@ -235,10 +241,13 @@ def run_theta_rho_file(file_path):
     ) as pbar:
         for i, coordinate in enumerate(coordinates):
             theta, rho = coordinate
+
             if state.stop_requested:
                 logger.info("Execution stopped by user")
                 if state.led_controller:
                     effect_idle(state.led_controller)
+                # Make sure to clear current_playing_file when stopping
+                state.current_playing_file = None
                 break
             
             if state.skip_requested:
@@ -246,6 +255,8 @@ def run_theta_rho_file(file_path):
                 connection_manager.check_idle()
                 if state.led_controller:
                     effect_idle(state.led_controller)
+                # Make sure to clear current_playing_file when skipping
+                state.current_playing_file = None
                 break
 
             # Wait for resume if paused
@@ -265,11 +276,20 @@ def run_theta_rho_file(file_path):
                 estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
                 elapsed_time = pbar.format_dict['elapsed']
                 state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
+                
+                # Send status updates based on time interval
+                current_time = time.time()
+                if current_time - last_status_update >= status_update_interval:
+                    last_status_update = current_time
 
     connection_manager.check_idle()
 
+    # Clear pattern state atomically
+
     state.current_playing_file = None
     state.execution_progress = None
+
+    
     logger.info("Pattern execution completed")
 
 def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
@@ -280,6 +300,7 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
     state.playlist_mode = run_mode
     state.current_playlist_index = 0
 
+
     try:
         while True:
             # Construct the complete pattern sequence
@@ -305,11 +326,15 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
 
             # Set the playlist to the first pattern
             state.current_playlist = pattern_sequence
+
             # Execute the pattern sequence
             for idx, file_path in enumerate(pattern_sequence):
                 state.current_playlist_index = idx
+
+                
                 if state.stop_requested:
                     logger.info("Execution stopped")
+
                     return
 
                 # Update state for main patterns only
@@ -322,14 +347,22 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
                 if idx < len(pattern_sequence) - 1 and not state.stop_requested and pause_time > 0 and not state.skip_requested:
                     logger.info(f"Pausing for {pause_time} seconds")
                     pause_start = time.time()
+                    last_status_update = time.time()
                     while time.time() - pause_start < pause_time:
                         if state.skip_requested:
                             logger.info("Pause interrupted by stop/skip request")
                             break
-                        time.sleep(1)
+                        
+                        # Periodically send status updates during long pauses
+                        current_time = time.time()
+                        if current_time - last_status_update >= 0.5:  # Update every 0.5 seconds
+                            last_status_update = current_time
+                            
+                        time.sleep(0.1)  # Use shorter sleep to check for skip more frequently
                     
                 state.skip_requested = False
 
+
             if run_mode == "indefinite":
                 logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
                 if pause_time > 0:
@@ -341,16 +374,7 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
                 break
 
     finally:
-        # Clean up progress update task
-        # if progress_update_task:
-        #     progress_update_task.cancel()
-        #     try:
-        #         await progress_update_task
-        #     except asyncio.CancelledError:
-        #         pass
-        #     progress_update_task = None
-            
-        # Clear all state variables
+
         state.current_playing_file = None
         state.execution_progress = None
         state.current_playlist = None
@@ -359,7 +383,8 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
         
         if state.led_controller:
             effect_idle(state.led_controller)
-        
+
+            
         logger.info("All requested patterns completed (or stopped) and state cleared")
 
 def stop_actions(clear_playlist = True):
@@ -379,21 +404,34 @@ def stop_actions(clear_playlist = True):
         connection_manager.update_machine_position()
 
 def get_status():
-    """Get the current execution status."""
-    # Update state.is_clearing based on current file
-    if state.current_playing_file in CLEAR_PATTERNS.values():
-        state.is_clearing = True
-    else:
-        state.is_clearing = False
-
-    return {
-        "ser_port": state.port,
-        "stop_requested": state.stop_requested,
-        "pause_requested": state.pause_requested,
-        "current_playing_file": state.current_playing_file,
-        "execution_progress": state.execution_progress,
-        "current_playing_index": state.current_playlist_index,
-        "current_playlist": state.current_playlist,
-        "is_clearing": state.is_clearing,
-        "current_speed": state.speed
+    """Get the current status of pattern execution."""
+    status = {
+        "current_file": state.current_playing_file,
+        "is_paused": state.pause_requested,
+        "is_running": bool(state.current_playing_file and not state.stop_requested),
+        "progress": None,
+        "playlist": None,
+        "speed": state.speed
     }
+    
+    # Add playlist information if available
+    if state.current_playlist and state.current_playlist_index is not None:
+        next_index = state.current_playlist_index + 1
+        status["playlist"] = {
+            "current_index": state.current_playlist_index,
+            "total_files": len(state.current_playlist),
+            "mode": state.playlist_mode,
+            "next_file": state.current_playlist[next_index] if next_index < len(state.current_playlist) else None
+        }
+    
+    # Only include progress information if a file is actually playing
+    if state.execution_progress and state.current_playing_file:
+        current, total, remaining_time, elapsed_time = state.execution_progress
+        status["progress"] = {
+            "current": current,
+            "total": total,
+            "remaining_time": remaining_time,
+            "elapsed_time": elapsed_time,
+            "percentage": (current / total * 100) if total > 0 else 0
+        }
+    return status

+ 58 - 97
static/js/main.js

@@ -257,12 +257,10 @@ async function runThetaRho() {
                 pre_execution: preExecutionAction 
             })
         });
-
+        
         const result = await response.json();
         if (response.ok) {
-            // Connect WebSocket when starting a pattern
-            connectStatusWebSocket();
-            
+
             // Show the currently playing UI immediately
             document.body.classList.add('playing');
             const currentlyPlayingFile = document.getElementById('currently-playing-file');
@@ -282,6 +280,9 @@ async function runThetaRho() {
     } catch (error) {
         logMessage(`Error running pattern: ${error.message}`, LOG_TYPE.ERROR);
     }
+    finally {
+        requestStatusUpdate();
+    }
 }
 
 async function stopExecution() {
@@ -548,7 +549,7 @@ async function runClearIn() {
                 pre_execution: 'none'
             })
         });
-        
+        requestStatusUpdate();
         if (!response.ok) {
             if (response.status === 409) {
                 logMessage('Cannot start pattern: Another pattern is already running', LOG_TYPE.WARNING);
@@ -559,7 +560,6 @@ async function runClearIn() {
         
         const result = await response.json();
         if (result.success) {
-            connectStatusWebSocket();
             document.body.classList.add('playing');
             logMessage('Clear from center pattern started', LOG_TYPE.SUCCESS);
         } else {
@@ -589,12 +589,12 @@ async function runClearOut() {
             }
             throw new Error(`HTTP error! status: ${response.status}`);
         }
-        
+        requestStatusUpdate();
         const result = await response.json();
         if (result.success) {
-            connectStatusWebSocket();
             document.body.classList.add('playing');
             logMessage('Clear from perimeter pattern started', LOG_TYPE.SUCCESS);
+
         } else {
             logMessage('Failed to run clear pattern', LOG_TYPE.ERROR);
         }
@@ -614,6 +614,7 @@ async function runClearSide() {
                 pre_execution: 'none'
             })
         });
+        requestStatusUpdate();
         
         if (!response.ok) {
             if (response.status === 409) {
@@ -625,7 +626,6 @@ async function runClearSide() {
         
         const result = await response.json();
         if (result.success) {
-            connectStatusWebSocket();
             document.body.classList.add('playing');
             logMessage('Clear sideways pattern started', LOG_TYPE.SUCCESS);
         } else {
@@ -666,6 +666,7 @@ function executeClearAction(actionFunction) {
 async function runFile(fileName) {
     const response = await fetch(`/run_theta_rho_file/${fileName}`, { method: 'POST' });
     const result = await response.json();
+    requestStatusUpdate();
     if (result.success) {
         logMessage(`Running file: ${fileName}`, LOG_TYPE.SUCCESS);
     } else {
@@ -938,7 +939,7 @@ async function runPlaylist() {
         logMessage('No playlist selected', 'error');
         return;
     }
-
+    
     const pauseTime = parseFloat(document.getElementById('pause_time').value) || 0;
     const clearPattern = document.getElementById('clear_pattern').value;
     const runMode = document.querySelector('input[name="run_mode"]:checked').value;
@@ -958,6 +959,7 @@ async function runPlaylist() {
                 shuffle: shuffle
             })
         });
+        requestStatusUpdate();
 
         if (!response.ok) {
             if (response.status === 409) {
@@ -969,8 +971,6 @@ async function runPlaylist() {
             return;
         }
 
-        // Connect WebSocket when starting a playlist
-        connectStatusWebSocket();
         
         logMessage(`Started playlist: ${playlistName}`, 'success');
         // Close the playlist editor container after successfully starting the playlist
@@ -1780,86 +1780,41 @@ const savedTheme = localStorage.getItem('theme') || 'light';
 document.documentElement.setAttribute('data-theme', savedTheme);
 themeIcon.className = savedTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
 
+// Function to request a status update from the server
+function requestStatusUpdate() {
+    // Request a fresh status update via AJAX
+    fetch('/status')
+        .then(response => response.json())
+        .then(data => {
+            updateCurrentlyPlayingUI(data);
+        })
+        .catch(error => {
+            console.error('Error requesting status update:', error);
+        });
+}
 
-// Add WebSocket connection for status updates
-let statusSocket = null;
-let reconnectAttempts = 0;
-const MAX_RECONNECT_ATTEMPTS = 5;
+// Variable to store the status update interval
 let statusUpdateInterval = null;
-let wsConnected = false;
 
-function connectStatusWebSocket() {
-    // Don't create a new connection if one already exists
-    if (wsConnected) {
-        console.log('WebSocket already connected');
-        return;
-    }
+// Function to start the status update interval
+function startStatusUpdates() {
+    // Clear any existing interval first
+    stopStatusUpdates();
+    
+    // Set up a new interval that runs every second
+    statusUpdateInterval = setInterval(requestStatusUpdate, 1000);
+    logMessage("Started automatic status updates", LOG_TYPE.DEBUG);
+}
 
-    // Close existing connection and clear interval if any
-    if (statusSocket) {
-        statusSocket.close();
-    }
+// Function to stop the status update interval
+function stopStatusUpdates() {
     if (statusUpdateInterval) {
         clearInterval(statusUpdateInterval);
-    }
-
-    // Create WebSocket connection
-    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-    statusSocket = new WebSocket(`${protocol}//${window.location.host}/ws/status`);
-
-    statusSocket.onopen = () => {
-        console.log('Status WebSocket connected');
-        wsConnected = true;
-        reconnectAttempts = 0; // Reset reconnect attempts on successful connection
-    };
-
-    statusSocket.onmessage = (event) => {
-        try {
-            const message = JSON.parse(event.data);
-            if (message.type === 'status_update' && message.data) {
-                updateCurrentlyPlayingUI(message.data);
-                
-                // Disconnect WebSocket if no pattern is running
-                if (!message.data.is_running) {
-                    console.log('No pattern running, disconnecting WebSocket');
-                    disconnectStatusWebSocket();
-                }
-            }
-        } catch (error) {
-            console.error('Error processing status update:', error);
-            console.error('Raw data that caused error:', event.data);
-        }
-    };
-
-    statusSocket.onclose = () => {
-        console.log('Status WebSocket disconnected');
-        wsConnected = false;
-        clearInterval(statusUpdateInterval);
-        
-        // Only attempt to reconnect if we're supposed to be connected
-        if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS && document.body.classList.contains('playing')) {
-            reconnectAttempts++;
-            const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
-            console.log(`Reconnecting in ${delay/1000}s (Attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
-            setTimeout(connectStatusWebSocket, delay);
-        }
-    };
-
-    statusSocket.onerror = (error) => {
-        console.error('WebSocket error:', error);
-    };
-}
-
-function disconnectStatusWebSocket() {
-    if (statusSocket && wsConnected) {
-        wsConnected = false;
-        statusSocket.close();
-        statusSocket = null;
-        reconnectAttempts = 0;
+        statusUpdateInterval = null;
+        logMessage("Stopped automatic status updates", LOG_TYPE.DEBUG);
     }
 }
 
-// Replace the polling mechanism with WebSocket
 document.addEventListener('DOMContentLoaded', () => {
     const activeTab = getCookie('activeTab') || 'patterns'; // Default to 'patterns' tab
     switchTab(activeTab); // Load the active tab
@@ -1870,30 +1825,18 @@ document.addEventListener('DOMContentLoaded', () => {
     attachFullScreenListeners();
     loadWledIp();
     updateWledUI();
-
-    // Initialize WebSocket connection for status updates
-    connectStatusWebSocket();
-
-    // Handle visibility change
-    document.addEventListener('visibilitychange', () => {
-        if (document.visibilityState === 'visible' && statusSocket && statusSocket.readyState !== WebSocket.OPEN) {
-            connectStatusWebSocket();
-        }
-    });
-
     checkForUpdates();
+    requestStatusUpdate(); // Request initial status update on page load
 });
 
 // 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) {
-    console.log('Updating UI with status:', status);
 
     // Get all required DOM elements once
     const container = document.getElementById('currently-playing-container');
@@ -1902,6 +1845,7 @@ function updateCurrentlyPlayingUI(status) {
     const progressText = document.getElementById('play_progress_text');
     const pausePlayButton = document.getElementById('pausePlayCurrent');
     const speedDisplay = document.getElementById('current_speed_display');
+    const skipButton = document.getElementById('skipCurrent');
 
     // Check if all required elements exist
     if (!container || !fileNameElement || !progressBar || !progressText) {
@@ -1915,6 +1859,18 @@ function updateCurrentlyPlayingUI(status) {
         return;
     }
 
+    // Start or stop status updates based on pattern running state
+    if (status.is_running || (status.playlist && status.playlist.next_file)) {
+        // Start status updates if they're not already running
+        if (!statusUpdateInterval) {
+            startStatusUpdates();
+        }
+    } else {
+        // Stop status updates if they're running
+        stopStatusUpdates();
+    }
+
+    // Always update the UI even if no pattern is playing
     // Update container visibility based on status
     if (status.current_file && status.is_running) {
         document.body.classList.add('playing');
@@ -1966,7 +1922,7 @@ function updateCurrentlyPlayingUI(status) {
     }
 
     // Update progress information
-    if (status.progress) {
+    if (status.progress && status.current_file) {
         const { percentage, remaining_time, elapsed_time } = status.progress;
         const formattedPercentage = percentage.toFixed(1);
         const remainingText = remaining_time === null ? 'calculating...' : formatSecondsToHMS(remaining_time);
@@ -1986,6 +1942,11 @@ function updateCurrentlyPlayingUI(status) {
             '<i class="fa-solid fa-pause"></i>';
     }
 
+    // Update skip button visibility
+    if (skipButton) {
+        skipButton.style.display = (status.playlist && status.playlist.next_file) ? 'inline-block' : 'none';
+    }
+
     // Update playlist UI if the function exists
     if (typeof updatePlaylistUI === 'function') {
         updatePlaylistUI(status);

+ 4 - 0
templates/index.html

@@ -420,6 +420,10 @@
     <!-- Messages will be appended here -->
 </div>
 
+<div id="debug-log" class="debug-log hidden">
+    <!-- Messages will be appended here -->
+</div>
+
 <script src="../static/js/main.js"></script>
 </body>
 </html>

+ 35 - 0
test_websocket.py

@@ -0,0 +1,35 @@
+import socketio
+
+# Create a Socket.IO client instance
+sio = socketio.Client()
+
+# Event handler for a successful connection on the /ws/status namespace
+@sio.event(namespace='/ws/status')
+def connect():
+    print("Connected to /ws/status namespace")
+
+# Event handler for connection errors on the /ws/status namespace
+@sio.event(namespace='/ws/status')
+def connect_error(data):
+    print("Connection failed:", data)
+
+# Event handler for disconnection from the /ws/status namespace
+@sio.event(namespace='/ws/status')
+def disconnect():
+    print("Disconnected from /ws/status namespace")
+
+# Listen for 'status_update' events in the /ws/status namespace
+@sio.on('status_update', namespace='/ws/status')
+def on_status_update(data):
+    print("Received status update:", data)
+
+if __name__ == "__main__":
+    # Replace with your server address if different
+    server_url = "http://192.168.2.131:8080"
+    try:
+        sio.connect(server_url, namespaces=['/ws/status'])
+        print("Waiting for status updates...")
+        # Keep the client running to listen for events
+        sio.wait()
+    except Exception as e:
+        print("An error occurred:", e)