1
0
tuanchris 9 сар өмнө
parent
commit
bebd41eccd

+ 6 - 5
main.py

@@ -218,11 +218,12 @@ async def broadcast_status_update(status: dict):
 
 
 @app.websocket("/ws/cache-progress")
 @app.websocket("/ws/cache-progress")
 async def websocket_cache_progress_endpoint(websocket: WebSocket):
 async def websocket_cache_progress_endpoint(websocket: WebSocket):
+    from modules.core.cache_manager import get_cache_progress
+
     await websocket.accept()
     await websocket.accept()
     active_cache_progress_connections.add(websocket)
     active_cache_progress_connections.add(websocket)
     try:
     try:
         while True:
         while True:
-            from modules.core.cache_manager import get_cache_progress
             progress = get_cache_progress()
             progress = get_cache_progress()
             try:
             try:
                 await websocket.send_json({
                 await websocket.send_json({
@@ -233,7 +234,7 @@ async def websocket_cache_progress_endpoint(websocket: WebSocket):
                 if "close message has been sent" in str(e):
                 if "close message has been sent" in str(e):
                     break
                     break
                 raise
                 raise
-            await asyncio.sleep(0.5)  # Update every 500ms
+            await asyncio.sleep(1.0)  # Update every 1 second (reduced frequency for better performance)
     except WebSocketDisconnect:
     except WebSocketDisconnect:
         pass
         pass
     finally:
     finally:
@@ -625,7 +626,7 @@ async def move_to_center():
 
 
         logger.info("Moving device to center position")
         logger.info("Moving device to center position")
         pattern_manager.reset_theta()
         pattern_manager.reset_theta()
-        pattern_manager.move_polar(0, 0)
+        await pattern_manager.move_polar(0, 0)
         return {"success": True}
         return {"success": True}
     except Exception as e:
     except Exception as e:
         logger.error(f"Failed to move to center: {str(e)}")
         logger.error(f"Failed to move to center: {str(e)}")
@@ -638,7 +639,7 @@ async def move_to_perimeter():
             logger.warning("Attempted to move to perimeter without a connection")
             logger.warning("Attempted to move to perimeter without a connection")
             raise HTTPException(status_code=400, detail="Connection not established")
             raise HTTPException(status_code=400, detail="Connection not established")
         pattern_manager.reset_theta()
         pattern_manager.reset_theta()
-        pattern_manager.move_polar(0, 1)
+        await pattern_manager.move_polar(0, 1)
         return {"success": True}
         return {"success": True}
     except Exception as e:
     except Exception as e:
         logger.error(f"Failed to move to perimeter: {str(e)}")
         logger.error(f"Failed to move to perimeter: {str(e)}")
@@ -747,7 +748,7 @@ async def send_coordinate(request: CoordinateRequest):
 
 
     try:
     try:
         logger.debug(f"Sending coordinate: theta={request.theta}, rho={request.rho}")
         logger.debug(f"Sending coordinate: theta={request.theta}, rho={request.rho}")
-        pattern_manager.move_polar(request.theta, request.rho)
+        await pattern_manager.move_polar(request.theta, request.rho)
         return {"success": True}
         return {"success": True}
     except Exception as e:
     except Exception as e:
         logger.error(f"Failed to send coordinate: {str(e)}")
         logger.error(f"Failed to send coordinate: {str(e)}")

+ 59 - 28
modules/connection/connection_manager.py

@@ -4,6 +4,7 @@ import logging
 import serial
 import serial
 import serial.tools.list_ports
 import serial.tools.list_ports
 import websocket
 import websocket
+import asyncio
 
 
 from modules.core.state import state
 from modules.core.state import state
 from modules.led.led_controller import effect_loading, effect_idle, effect_connected, LEDController
 from modules.led.led_controller import effect_loading, effect_idle, effect_connected, LEDController
@@ -71,7 +72,14 @@ class SerialConnection(BaseConnection):
         return self.ser is not None and self.ser.is_open
         return self.ser is not None and self.ser.is_open
 
 
     def close(self) -> None:
     def close(self) -> None:
-        update_machine_position()
+        # Run async update_machine_position in sync context
+        try:
+            loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(loop)
+            loop.run_until_complete(update_machine_position())
+            loop.close()
+        except Exception as e:
+            logger.error(f"Error updating machine position on close: {e}")
         with self.lock:
         with self.lock:
             if self.ser.is_open:
             if self.ser.is_open:
                 self.ser.close()
                 self.ser.close()
@@ -119,7 +127,14 @@ class WebSocketConnection(BaseConnection):
         return self.ws is not None
         return self.ws is not None
 
 
     def close(self) -> None:
     def close(self) -> None:
-        update_machine_position()
+        # Run async update_machine_position in sync context
+        try:
+            loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(loop)
+            loop.run_until_complete(update_machine_position())
+            loop.close()
+        except Exception as e:
+            logger.error(f"Error updating machine position on close: {e}")
         with self.lock:
         with self.lock:
             if self.ws:
             if self.ws:
                 self.ws.close()
                 self.ws.close()
@@ -220,24 +235,26 @@ def parse_machine_position(response: str):
     return None
     return None
 
 
 
 
-def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
+async def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
     """
     """
     Send a G-code command to FluidNC and wait for an 'ok' response.
     Send a G-code command to FluidNC and wait for an 'ok' response.
     If no response after set timeout, sets state to stop and disconnects.
     If no response after set timeout, sets state to stop and disconnects.
     """
     """
     logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
     logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
-    
+
     # Track overall attempt time
     # Track overall attempt time
     overall_start_time = time.time()
     overall_start_time = time.time()
-    
+
     while True:
     while True:
         try:
         try:
             gcode = f"$J=G91 G21 Y{y} F{speed}" if home else f"G1 X{x} Y{y} F{speed}"
             gcode = f"$J=G91 G21 Y{y} F{speed}" if home else f"G1 X{x} Y{y} F{speed}"
-            state.conn.send(gcode + "\n")
+            # Use asyncio.to_thread for both send and receive operations to avoid blocking
+            await asyncio.to_thread(state.conn.send, gcode + "\n")
             logger.debug(f"Sent command: {gcode}")
             logger.debug(f"Sent command: {gcode}")
             start_time = time.time()
             start_time = time.time()
             while True:
             while True:
-                response = state.conn.readline()
+                # Use asyncio.to_thread for blocking I/O operations
+                response = await asyncio.to_thread(state.conn.readline)
                 logger.debug(f"Response: {response}")
                 logger.debug(f"Response: {response}")
                 if response.lower() == "ok":
                 if response.lower() == "ok":
                     logger.debug("Command execution confirmed.")
                     logger.debug("Command execution confirmed.")
@@ -246,7 +263,7 @@ def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
             # Store the error string inside the exception block
             # Store the error string inside the exception block
             error_str = str(e)
             error_str = str(e)
             logger.warning(f"Error sending command: {error_str}")
             logger.warning(f"Error sending command: {error_str}")
-            
+
             # Immediately return for device not configured errors
             # Immediately return for device not configured errors
             if "Device not configured" in error_str or "Errno 6" in error_str:
             if "Device not configured" in error_str or "Errno 6" in error_str:
                 logger.error(f"Device configuration error detected: {error_str}")
                 logger.error(f"Device configuration error detected: {error_str}")
@@ -256,9 +273,9 @@ def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
                 logger.info("Connection marked as disconnected due to device error")
                 logger.info("Connection marked as disconnected due to device error")
                 return False
                 return False
 
 
-            
+
         logger.warning(f"No 'ok' received for X{x} Y{y}, speed {speed}. Retrying...")
         logger.warning(f"No 'ok' received for X{x} Y{y}, speed {speed}. Retrying...")
-        time.sleep(0.1)
+        await asyncio.sleep(0.1)
     
     
     # If we reach here, the timeout has occurred
     # If we reach here, the timeout has occurred
     logger.error(f"Failed to receive 'ok' response after {max_total_attempt_time} seconds. Stopping and disconnecting.")
     logger.error(f"Failed to receive 'ok' response after {max_total_attempt_time} seconds. Stopping and disconnecting.")
@@ -406,20 +423,27 @@ def home(timeout=15):
                     homing_speed = 120
                     homing_speed = 120
                 logger.info("Sensorless homing not supported. Using crash homing")
                 logger.info("Sensorless homing not supported. Using crash homing")
                 logger.info(f"Homing with speed {homing_speed}")
                 logger.info(f"Homing with speed {homing_speed}")
-                if state.gear_ratio == 6.25:
-                    result = send_grbl_coordinates(0, - 30, homing_speed, home=True)
-                    if result == False:
-                        logger.error("Homing failed - send_grbl_coordinates returned False")
-                        homing_complete.set()
-                        return
-                    state.machine_y -= 30
-                else:
-                    result = send_grbl_coordinates(0, -22, homing_speed, home=True)
-                    if result == False:
-                        logger.error("Homing failed - send_grbl_coordinates returned False")
-                        homing_complete.set()
-                        return
-                    state.machine_y -= 22
+
+                # Run async function in new event loop
+                loop = asyncio.new_event_loop()
+                asyncio.set_event_loop(loop)
+                try:
+                    if state.gear_ratio == 6.25:
+                        result = loop.run_until_complete(send_grbl_coordinates(0, - 30, homing_speed, home=True))
+                        if result == False:
+                            logger.error("Homing failed - send_grbl_coordinates returned False")
+                            homing_complete.set()
+                            return
+                        state.machine_y -= 30
+                    else:
+                        result = loop.run_until_complete(send_grbl_coordinates(0, -22, homing_speed, home=True))
+                        if result == False:
+                            logger.error("Homing failed - send_grbl_coordinates returned False")
+                            homing_complete.set()
+                            return
+                        state.machine_y -= 22
+                finally:
+                    loop.close()
 
 
             state.current_theta = state.current_rho = 0
             state.current_theta = state.current_rho = 0
             homing_success = True
             homing_success = True
@@ -462,7 +486,14 @@ def check_idle():
         response = get_status_response()
         response = get_status_response()
         if response and "Idle" in response:
         if response and "Idle" in response:
             logger.info("Device is idle")
             logger.info("Device is idle")
-            update_machine_position()
+            # Run async update_machine_position in sync context
+            try:
+                loop = asyncio.new_event_loop()
+                asyncio.set_event_loop(loop)
+                loop.run_until_complete(update_machine_position())
+                loop.close()
+            except Exception as e:
+                logger.error(f"Error updating machine position: {e}")
             return True
             return True
         time.sleep(1)
         time.sleep(1)
         
         
@@ -490,12 +521,12 @@ def get_machine_position(timeout=5):
     logger.warning("Timeout reached waiting for machine position")
     logger.warning("Timeout reached waiting for machine position")
     return None, None
     return None, None
 
 
-def update_machine_position():
+async def update_machine_position():
     if (state.conn.is_connected() if state.conn else False):
     if (state.conn.is_connected() if state.conn else False):
         try:
         try:
             logger.info('Saving machine position')
             logger.info('Saving machine position')
-            state.machine_x, state.machine_y = get_machine_position()
-            state.save()
+            state.machine_x, state.machine_y = await asyncio.to_thread(get_machine_position)
+            await asyncio.to_thread(state.save)
             logger.info(f'Machine position saved: {state.machine_x}, {state.machine_y}')
             logger.info(f'Machine position saved: {state.machine_x}, {state.machine_y}')
         except Exception as e:
         except Exception as e:
             logger.error(f"Error updating machine position: {e}")
             logger.error(f"Error updating machine position: {e}")

+ 6 - 2
modules/core/cache_manager.py

@@ -731,9 +731,13 @@ async def generate_cache_background():
         raise
         raise
 
 
 def get_cache_progress():
 def get_cache_progress():
-    """Get the current cache generation progress."""
+    """Get the current cache generation progress.
+
+    Returns a reference to the cache_progress dict for read-only access.
+    The WebSocket handler should not modify this dict.
+    """
     global cache_progress
     global cache_progress
-    return cache_progress.copy()
+    return cache_progress  # Return reference instead of copy for better performance
 
 
 def is_cache_generation_needed():
 def is_cache_generation_needed():
     """Check if cache generation is needed."""
     """Check if cache generation is needed."""

+ 44 - 24
modules/core/pattern_manager.py

@@ -345,7 +345,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                 else:
                 else:
                     current_speed = state.speed
                     current_speed = state.speed
                     
                     
-                move_polar(theta, rho, current_speed)
+                await move_polar(theta, rho, current_speed)
                 
                 
                 # Update progress for all coordinates including the first one
                 # Update progress for all coordinates including the first one
                 pbar.update(1)
                 pbar.update(1)
@@ -518,36 +518,49 @@ def stop_actions(clear_playlist = True):
             state.current_playing_file = None
             state.current_playing_file = None
             state.execution_progress = None
             state.execution_progress = None
             state.is_clearing = False
             state.is_clearing = False
-            
+
             if clear_playlist:
             if clear_playlist:
                 # Clear playlist state
                 # Clear playlist state
                 state.current_playlist = None
                 state.current_playlist = None
                 state.current_playlist_index = None
                 state.current_playlist_index = None
                 state.playlist_mode = None
                 state.playlist_mode = None
-                
+
                 # Cancel progress update task if we're clearing the playlist
                 # Cancel progress update task if we're clearing the playlist
                 global progress_update_task
                 global progress_update_task
                 if progress_update_task and not progress_update_task.done():
                 if progress_update_task and not progress_update_task.done():
                     progress_update_task.cancel()
                     progress_update_task.cancel()
-                
+
             state.pause_condition.notify_all()
             state.pause_condition.notify_all()
-            connection_manager.update_machine_position()
+            # Run async function in sync context
+            try:
+                loop = asyncio.new_event_loop()
+                asyncio.set_event_loop(loop)
+                loop.run_until_complete(connection_manager.update_machine_position())
+                loop.close()
+            except Exception as update_err:
+                logger.error(f"Error updating machine position: {update_err}")
     except Exception as e:
     except Exception as e:
         logger.error(f"Error during stop_actions: {e}")
         logger.error(f"Error during stop_actions: {e}")
         # Ensure we still update machine position even if there's an error
         # Ensure we still update machine position even if there's an error
-        connection_manager.update_machine_position()
-
-def move_polar(theta, rho, speed=None):
+        try:
+            loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(loop)
+            loop.run_until_complete(connection_manager.update_machine_position())
+            loop.close()
+        except Exception as update_err:
+            logger.error(f"Error updating machine position on error: {update_err}")
+
+async def move_polar(theta, rho, speed=None):
     """
     """
     This functions take in a pair of theta rho coordinate, compute the distance to travel based on current theta, rho,
     This functions take in a pair of theta rho coordinate, compute the distance to travel based on current theta, rho,
-    and translate the motion to gcode jog command and sent to grbl. 
-    
-    Since having similar steps_per_mm will make x and y axis moves at around the same speed, we have to scale the 
+    and translate the motion to gcode jog command and sent to grbl.
+
+    Since having similar steps_per_mm will make x and y axis moves at around the same speed, we have to scale the
     x_steps_per_mm and y_steps_per_mm so that they are roughly the same. Here's the range of motion:
     x_steps_per_mm and y_steps_per_mm so that they are roughly the same. Here's the range of motion:
-    
+
     X axis (angular): 50mm = 1 revolution
     X axis (angular): 50mm = 1 revolution
     Y axis (radial): 0 => 20mm = theta 0 (center) => 1 (perimeter)
     Y axis (radial): 0 => 20mm = theta 0 (center) => 1 (perimeter)
-    
+
     Args:
     Args:
         theta (_type_): _description_
         theta (_type_): _description_
         rho (_type_): _description_
         rho (_type_): _description_
@@ -557,42 +570,42 @@ def move_polar(theta, rho, speed=None):
     # soft_limit_inner = 0.01
     # soft_limit_inner = 0.01
     # if rho < soft_limit_inner:
     # if rho < soft_limit_inner:
     #     rho = soft_limit_inner
     #     rho = soft_limit_inner
-    
+
     # soft_limit_outter = 0.015
     # soft_limit_outter = 0.015
     # if rho > (1-soft_limit_outter):
     # if rho > (1-soft_limit_outter):
     #     rho = (1-soft_limit_outter)
     #     rho = (1-soft_limit_outter)
-    
+
     if state.table_type == 'dune_weaver_mini':
     if state.table_type == 'dune_weaver_mini':
         x_scaling_factor = 2
         x_scaling_factor = 2
         y_scaling_factor = 3.7
         y_scaling_factor = 3.7
     else:
     else:
         x_scaling_factor = 2
         x_scaling_factor = 2
         y_scaling_factor = 5
         y_scaling_factor = 5
-    
+
     delta_theta = theta - state.current_theta
     delta_theta = theta - state.current_theta
     delta_rho = rho - state.current_rho
     delta_rho = rho - state.current_rho
     x_increment = delta_theta * 100 / (2 * pi * x_scaling_factor)  # Added -1 to reverse direction
     x_increment = delta_theta * 100 / (2 * pi * x_scaling_factor)  # Added -1 to reverse direction
     y_increment = delta_rho * 100 / y_scaling_factor
     y_increment = delta_rho * 100 / y_scaling_factor
-    
+
     x_total_steps = state.x_steps_per_mm * (100/x_scaling_factor)
     x_total_steps = state.x_steps_per_mm * (100/x_scaling_factor)
     y_total_steps = state.y_steps_per_mm * (100/y_scaling_factor)
     y_total_steps = state.y_steps_per_mm * (100/y_scaling_factor)
-        
+
     offset = x_increment * (x_total_steps * x_scaling_factor / (state.gear_ratio * y_total_steps * y_scaling_factor))
     offset = x_increment * (x_total_steps * x_scaling_factor / (state.gear_ratio * y_total_steps * y_scaling_factor))
 
 
     if state.table_type == 'dune_weaver_mini' or state.y_steps_per_mm == 546:
     if state.table_type == 'dune_weaver_mini' or state.y_steps_per_mm == 546:
         y_increment -= offset
         y_increment -= offset
     else:
     else:
         y_increment += offset
         y_increment += offset
-    
+
     new_x_abs = state.machine_x + x_increment
     new_x_abs = state.machine_x + x_increment
     new_y_abs = state.machine_y + y_increment
     new_y_abs = state.machine_y + y_increment
-    
+
     # Use provided speed or fall back to state.speed
     # Use provided speed or fall back to state.speed
     actual_speed = speed if speed is not None else state.speed
     actual_speed = speed if speed is not None else state.speed
-    
+
     # dynamic_speed = compute_dynamic_speed(rho, max_speed=actual_speed)
     # dynamic_speed = compute_dynamic_speed(rho, max_speed=actual_speed)
-    
-    connection_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), actual_speed)
+
+    await connection_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), actual_speed)
     state.current_theta = theta
     state.current_theta = theta
     state.current_rho = rho
     state.current_rho = rho
     state.machine_x = new_x_abs
     state.machine_x = new_x_abs
@@ -615,7 +628,14 @@ def resume_execution():
 def reset_theta():
 def reset_theta():
     logger.info('Resetting Theta')
     logger.info('Resetting Theta')
     state.current_theta = state.current_theta % (2 * pi)
     state.current_theta = state.current_theta % (2 * pi)
-    connection_manager.update_machine_position()
+    # Run async function in sync context
+    try:
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        loop.run_until_complete(connection_manager.update_machine_position())
+        loop.close()
+    except Exception as e:
+        logger.error(f"Error updating machine position in reset_theta: {e}")
 
 
 def set_speed(new_speed):
 def set_speed(new_speed):
     state.speed = new_speed
     state.speed = new_speed