Parcourir la source

Revert to stable serial/motion code from commit 3143609

Reverts complex timeout recovery and retry logic that was causing issues.
Keeps working features from HEAD:
- get_last_completed_execution_time() for historical ETA
- connect_device() with DW LED support and auto-connect modes
- MPos/WPos dual format support for GRBL compatibility
- DEPRIORITIZED_PORTS for smarter port selection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris il y a 1 semaine
Parent
commit
5315670e47
3 fichiers modifiés avec 391 ajouts et 1215 suppressions
  1. 1 1
      main.py
  2. 122 531
      modules/connection/connection_manager.py
  3. 268 683
      modules/core/pattern_manager.py

+ 1 - 1
main.py

@@ -3175,7 +3175,7 @@ async def preview_thr_batch(request: dict):
     # Convert results to dictionary
     results = dict(file_results)
 
-    logger.info(f"Total batch processing time: {time.time() - start:.2f}s for {len(file_names)} files")
+    logger.debug(f"Total batch processing time: {time.time() - start:.2f}s for {len(file_names)} files")
     return JSONResponse(content=results, headers=headers)
 
 @app.get("/playlists")

+ 122 - 531
modules/connection/connection_manager.py

@@ -16,8 +16,9 @@ logger = logging.getLogger(__name__)
 
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
 
-# Ports to deprioritize during auto-connect (shown in UI but not auto-selected)
-DEPRIORITIZED_PORTS = ['/dev/ttyS0']
+# Ports to deprioritize during auto-connect (e.g., hardware UART vs USB serial)
+# These will only be used if no other ports are available
+DEPRIORITIZED_PORTS = ['/dev/ttyAMA0', '/dev/serial0']
 
 
 async def _check_table_is_idle() -> bool:
@@ -94,12 +95,6 @@ class SerialConnection(BaseConnection):
         with self.lock:
             return self.ser.in_waiting
 
-    def reset_input_buffer(self) -> None:
-        """Clear any stale data from the serial input buffer."""
-        with self.lock:
-            if self.ser and self.ser.is_open:
-                self.ser.reset_input_buffer()
-
     def is_connected(self) -> bool:
         return self.ser is not None and self.ser.is_open
 
@@ -125,6 +120,8 @@ class SerialConnection(BaseConnection):
         with self.lock:
             if self.ser.is_open:
                 self.ser.close()
+        # Release the lock resources
+        self.lock = None
 
 ###############################################################################
 # WebSocket Connection Implementation
@@ -188,7 +185,9 @@ class WebSocketConnection(BaseConnection):
         with self.lock:
             if self.ws:
                 self.ws.close()
-
+        # Release the lock resources
+        self.lock = None
+                
 def list_serial_ports():
     """Return a list of available serial ports."""
     ports = serial.tools.list_ports.comports()
@@ -200,7 +199,7 @@ def device_init(homing=True):
     try:
         if get_machine_steps():
             logger.info(f"x_steps_per_mm: {state.x_steps_per_mm}, y_steps_per_mm: {state.y_steps_per_mm}, gear_ratio: {state.gear_ratio}")
-        else:
+        else: 
             logger.fatal("Failed to get machine steps")
             state.conn.close()
             return False
@@ -209,10 +208,6 @@ def device_init(homing=True):
         state.conn.close()
         return False
 
-    # Reset work coordinate offsets for a clean start
-    # This ensures we're using work coordinates (G54) starting from 0
-    reset_work_coordinates()
-
     machine_x, machine_y = get_machine_position()
     if machine_x != state.machine_x or machine_y != state.machine_y:
         logger.info(f'x, y; {machine_x}, {machine_y}')
@@ -228,7 +223,6 @@ def device_init(homing=True):
         logger.info(f'State x, y; {state.machine_x}, {state.machine_y}')
 
     time.sleep(2)  # Allow time for the connection to establish
-    return True
 
 
 def connect_device(homing=True):
@@ -402,7 +396,6 @@ def get_status_response() -> str:
     if state.conn is None or not state.conn.is_connected():
         logger.warning("Cannot get status response: no active connection")
         return False
-
     while True:
         try:
             state.conn.send('?')
@@ -445,66 +438,32 @@ def parse_machine_position(response: str):
     return None
 
 
-async def send_grbl_coordinates(x, y, speed=600, timeout=30, 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.
-    If no response after set timeout, returns False.
-
-    Args:
-        x: X coordinate
-        y: Y coordinate
-        speed: Feed rate in mm/min
-        timeout: Maximum time in seconds to wait for 'ok' response
-        home: If True, sends jog command ($J=) instead of G1
-
-    Returns:
-        True on success, False on timeout or error
+    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}")
 
+    # Track overall attempt time
     overall_start_time = time.time()
-    max_retries = 3
-    retry_count = 0
-
-    while retry_count < max_retries:
-        # Check overall timeout
-        if time.time() - overall_start_time > timeout:
-            logger.error(f"Timeout waiting for 'ok' response after {timeout}s")
-            return False
 
+    while True:
         try:
-            gcode = f"$J=G91 G21 Y{y:.2f} F{speed}" if home else f"G1 X{x:.2f} Y{y:.2f} F{speed}"
+            gcode = f"$J=G91 G21 Y{y} F{speed}" if home else f"G1 X{x} Y{y} F{speed}"
+            # 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}")
-
-            # Wait for 'ok' response with timeout
-            response_start = time.time()
-            response_timeout = min(10, timeout - (time.time() - overall_start_time))
-
-            while time.time() - response_start < response_timeout:
-                # Check overall timeout
-                if time.time() - overall_start_time > timeout:
-                    logger.error(f"Overall timeout waiting for 'ok' response")
-                    return False
-
+            start_time = time.time()
+            while True:
+                # Use asyncio.to_thread for blocking I/O operations
                 response = await asyncio.to_thread(state.conn.readline)
-                if response:
-                    logger.debug(f"Response: {response}")
-                    if response.lower().strip() == "ok":
-                        logger.debug("Command execution confirmed.")
-                        return True
-                    elif 'error' in response.lower():
-                        logger.warning(f"Got error response: {response}")
-                        # Don't immediately fail - some errors are recoverable
-                else:
-                    await asyncio.sleep(0.05)
-
-            # Response timeout for this attempt
-            logger.warning(f"No 'ok' received for {gcode}, retrying... ({retry_count + 1}/{max_retries})")
-            retry_count += 1
-            await asyncio.sleep(0.2)
-
+                logger.debug(f"Response: {response}")
+                if response.lower() == "ok":
+                    logger.debug("Command execution confirmed.")
+                    return
         except Exception as e:
+            # Store the error string inside the exception block
             error_str = str(e)
             logger.warning(f"Error sending command: {error_str}")
 
@@ -517,307 +476,42 @@ async def send_grbl_coordinates(x, y, speed=600, timeout=30, home=False):
                 logger.info("Connection marked as disconnected due to device error")
                 return False
 
-            retry_count += 1
-            await asyncio.sleep(0.2)
-
-    logger.error(f"Failed to receive 'ok' response after {max_retries} retries")
-    return False
-
-
-def _detect_firmware():
-    """
-    Detect firmware type (FluidNC or GRBL) by sending $I command.
-    Returns tuple: (firmware_type: str, version: str or None)
-    firmware_type is 'fluidnc', 'grbl', or 'unknown'
-    """
-    if not state.conn or not state.conn.is_connected():
-        return ('unknown', None)
-
-    # Clear buffer first
-    try:
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-    except Exception:
-        pass
-
-    try:
-        state.conn.send("$I\n")
-        time.sleep(0.3)
-
-        firmware_type = 'unknown'
-        version = None
-        start_time = time.time()
-
-        while time.time() - start_time < 2.0:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"Firmware detection response: {response}")
-                    response_lower = response.lower()
-
-                    if 'fluidnc' in response_lower:
-                        firmware_type = 'fluidnc'
-                        # Try to extract version from response like "FluidNC v3.7.2"
-                        if 'v' in response_lower:
-                            parts = response.split()
-                            for part in parts:
-                                if part.lower().startswith('v') and any(c.isdigit() for c in part):
-                                    version = part
-                                    break
-                        break
-                    elif 'grbl' in response_lower and 'fluidnc' not in response_lower:
-                        firmware_type = 'grbl'
-                        # Try to extract version like "Grbl 1.1h"
-                        parts = response.split()
-                        for i, part in enumerate(parts):
-                            if 'grbl' in part.lower() and i + 1 < len(parts):
-                                version = parts[i + 1]
-                                break
-                        break
-                    elif response.lower().strip() == 'ok':
-                        break
-            else:
-                time.sleep(0.05)
-
-        # Clear any remaining responses
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-
-        return (firmware_type, version)
-
-    except Exception as e:
-        logger.warning(f"Firmware detection failed: {e}")
-        return ('unknown', None)
-
-
-def _get_steps_fluidnc():
-    """
-    Get steps/mm from FluidNC using individual setting queries.
-    Returns tuple: (x_steps_per_mm, y_steps_per_mm) or (None, None) on failure.
-
-    Note: Works even when device is in ALARM state (e.g., limit switch active).
-    """
-    x_steps = None
-    y_steps = None
-
-    # Clear buffer
-    try:
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-    except Exception:
-        pass
-
-    # Query X steps/mm
-    try:
-        state.conn.send("$/axes/x/steps_per_mm\n")
-        time.sleep(0.2)
-
-        start_time = time.time()
-        while time.time() - start_time < 2.0:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"FluidNC X steps response: {response}")
-                    # Response format: "/axes/x/steps_per_mm=200.000" or similar
-                    if 'steps_per_mm=' in response:
-                        try:
-                            x_steps = float(response.split('=')[1].strip())
-                            state.x_steps_per_mm = x_steps
-                            logger.info(f"X steps per mm (FluidNC): {x_steps}")
-                        except (ValueError, IndexError) as e:
-                            logger.warning(f"Failed to parse X steps: {e}")
-                        break
-                    elif response.lower().strip() == 'ok':
-                        break
-                    elif 'error' in response.lower() or 'alarm' in response.lower():
-                        # Device may be in alarm state (e.g., limit switch active)
-                        # Log and continue - settings queries often work anyway
-                        logger.debug(f"Got error/alarm response, continuing: {response}")
-            else:
-                time.sleep(0.05)
-    except Exception as e:
-        logger.error(f"Error querying FluidNC X steps: {e}")
-
-    # Clear buffer before next query
-    try:
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-    except Exception:
-        pass
-
-    # Query Y steps/mm
-    try:
-        state.conn.send("$/axes/y/steps_per_mm\n")
-        time.sleep(0.2)
-
-        start_time = time.time()
-        while time.time() - start_time < 2.0:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"FluidNC Y steps response: {response}")
-                    if 'steps_per_mm=' in response:
-                        try:
-                            y_steps = float(response.split('=')[1].strip())
-                            state.y_steps_per_mm = y_steps
-                            logger.info(f"Y steps per mm (FluidNC): {y_steps}")
-                        except (ValueError, IndexError) as e:
-                            logger.warning(f"Failed to parse Y steps: {e}")
-                        break
-                    elif response.lower().strip() == 'ok':
-                        break
-                    elif 'error' in response.lower() or 'alarm' in response.lower():
-                        logger.debug(f"Got error/alarm response, continuing: {response}")
-            else:
-                time.sleep(0.05)
-    except Exception as e:
-        logger.error(f"Error querying FluidNC Y steps: {e}")
-
-    # Clear buffer before homing query
-    try:
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-    except Exception:
-        pass
-
-    # Query homing cycle setting (informational - user preference takes precedence)
-    try:
-        state.conn.send("$/axes/y/homing/cycle\n")
-        time.sleep(0.2)
-
-        start_time = time.time()
-        while time.time() - start_time < 1.5:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"FluidNC homing response: {response}")
-                    if 'homing/cycle=' in response:
-                        try:
-                            homing_cycle = int(float(response.split('=')[1].strip()))
-                            # cycle >= 1 means homing is enabled in firmware
-                            firmware_homing = 1 if homing_cycle >= 1 else 0
-                            logger.info(f"Firmware homing setting (cycle): {homing_cycle}, using user preference: {state.homing}")
-                        except (ValueError, IndexError):
-                            pass
-                        break
-                    elif response.lower().strip() == 'ok':
-                        break
-            else:
-                time.sleep(0.05)
-    except Exception as e:
-        logger.debug(f"Could not query FluidNC homing setting: {e}")
-
-    # Clear buffer
-    try:
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-    except Exception:
-        pass
-
-    return (x_steps, y_steps)
-
-
-def _get_steps_grbl():
-    """
-    Get steps/mm from GRBL using $$ command.
-    Returns tuple: (x_steps_per_mm, y_steps_per_mm) or (None, None) on failure.
-
-    Note: Works even when device is in ALARM state (e.g., limit switch active).
-    $$ command typically responds with settings even during alarm.
-    """
-    x_steps_per_mm = None
-    y_steps_per_mm = None
-
-    max_retries = 3
-    attempt_timeout = 4
-
-    for attempt in range(max_retries):
-        logger.info(f"Requesting GRBL settings with $$ command (attempt {attempt + 1}/{max_retries})")
 
+        logger.warning(f"No 'ok' received for X{x} Y{y}, speed {speed}. Retrying...")
+        await asyncio.sleep(0.1)
+    
+    # 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.")
+    
+    # Set state to stop
+    state.stop_requested = True
+    
+    # Set connection status to disconnected
+    if state.conn:
         try:
-            state.conn.send("$$\n")
-        except Exception as e:
-            logger.error(f"Error sending $$ command: {e}")
-            continue
-
-        attempt_start = time.time()
-        got_ok = False
-
-        while time.time() - attempt_start < attempt_timeout:
-            try:
-                response = state.conn.readline()
-
-                if not response:
-                    continue
-
-                logger.debug(f"Raw response: {response}")
-
-                for line in response.splitlines():
-                    line = line.strip()
-                    if not line:
-                        continue
-
-                    logger.debug(f"Config response: {line}")
-
-                    if line.startswith("$100="):
-                        x_steps_per_mm = float(line.split("=")[1])
-                        state.x_steps_per_mm = x_steps_per_mm
-                        logger.info(f"X steps per mm: {x_steps_per_mm}")
-                    elif line.startswith("$101="):
-                        y_steps_per_mm = float(line.split("=")[1])
-                        state.y_steps_per_mm = y_steps_per_mm
-                        logger.info(f"Y steps per mm: {y_steps_per_mm}")
-                    elif line.startswith("$22="):
-                        firmware_homing = int(line.split('=')[1])
-                        logger.info(f"Firmware homing setting ($22): {firmware_homing}, using user preference: {state.homing}")
-                    elif line.lower() == 'ok':
-                        got_ok = True
-                        logger.debug("Received 'ok' confirmation from GRBL")
-                    elif line.lower().startswith('error') or 'alarm' in line.lower():
-                        # Device may be in alarm state (e.g., limit switch active)
-                        # Log and continue - $$ typically works anyway
-                        logger.debug(f"Got error/alarm during settings query (proceeding): {line}")
-
-                if got_ok:
-                    if x_steps_per_mm is not None and y_steps_per_mm is not None:
-                        logger.info("Successfully received all GRBL settings")
-                        break
-                    else:
-                        logger.warning("Received 'ok' but missing some settings")
-                        break
-
-            except Exception as e:
-                logger.error(f"Error reading GRBL response: {e}")
-                break
-
-        if x_steps_per_mm is not None and y_steps_per_mm is not None:
-            break
-
-        if attempt < max_retries - 1:
-            logger.warning(f"Attempt {attempt + 1} did not get all settings, retrying...")
-            time.sleep(0.5)
-            try:
-                while state.conn.in_waiting() > 0:
-                    state.conn.readline()
-            except Exception:
-                pass
-
-    return (x_steps_per_mm, y_steps_per_mm)
-
+            state.conn.disconnect()
+        except:
+            pass
+        state.conn = None
+        
+    # Update the state connection status
+    state.is_connected = False
+    logger.info("Connection marked as disconnected due to timeout")
+    return False
 
 def get_machine_steps(timeout=10):
     """
-    Get machine steps/mm from the controller (FluidNC or GRBL).
+    Get machine steps/mm from the GRBL controller.
     Returns True if successful, False otherwise.
-
-    Detects firmware type first:
-    - FluidNC: Uses targeted $/axes/x/steps_per_mm queries (more reliable)
-    - GRBL: Falls back to $$ command with retries
     """
     if not state.conn or not state.conn.is_connected():
         logger.error("Cannot get machine steps: No connection available")
         return False
 
+    x_steps_per_mm = None
+    y_steps_per_mm = None
+    start_time = time.time()
+
     # Clear any pending data in the buffer
     try:
         while state.conn.in_waiting() > 0:
@@ -825,62 +519,64 @@ def get_machine_steps(timeout=10):
     except Exception as e:
         logger.warning(f"Error clearing buffer: {e}")
 
-    # Verify controller is responsive before querying
+    # Send the command to request all settings
     try:
-        state.conn.send("?\n")
-        time.sleep(0.2)
-        ready_check_attempts = 5
-        controller_ready = False
-        in_alarm = False
-        for _ in range(ready_check_attempts):
+        logger.info("Requesting GRBL settings with $$ command")
+        state.conn.send("$$\n")
+        time.sleep(1.0)  # Give GRBL time to process and respond (ESP32/FluidNC may need longer)
+    except Exception as e:
+        logger.error(f"Error sending $$ command: {e}")
+        return False
+
+    # Wait for and process responses
+    settings_complete = False
+    last_retry_time = start_time  # Track when we last sent $$ for retry logic
+    while time.time() - start_time < timeout and not settings_complete:
+        try:
+            # Attempt to read a line from the connection
             if state.conn.in_waiting() > 0:
                 response = state.conn.readline()
-                if response and ('<' in response or 'Idle' in response or 'Alarm' in response):
-                    controller_ready = True
-                    if 'Alarm' in response:
-                        in_alarm = True
-                        logger.info(f"Controller in ALARM state (likely limit switch active), proceeding with settings query: {response.strip()}")
-                    else:
-                        logger.debug(f"Controller ready, status: {response}")
-                    break
-            time.sleep(0.1)
-
-        if not controller_ready:
-            logger.warning("Controller not responding to status query, proceeding anyway...")
-
-        # Clear buffer after readiness check
-        while state.conn.in_waiting() > 0:
-            state.conn.readline()
-        time.sleep(0.1)
-    except Exception as e:
-        logger.warning(f"Readiness check failed: {e}, proceeding anyway...")
+                logger.debug(f"Raw response: {response}")
 
-    # Detect firmware type
-    firmware_type, firmware_version = _detect_firmware()
+                # Process the line
+                if response.strip():  # Only process non-empty lines
+                    for line in response.splitlines():
+                        line = line.strip()
+                        logger.debug(f"Config response: {line}")
+                        if line.startswith("$100="):
+                            x_steps_per_mm = float(line.split("=")[1])
+                            state.x_steps_per_mm = x_steps_per_mm
+                            logger.info(f"X steps per mm: {x_steps_per_mm}")
+                        elif line.startswith("$101="):
+                            y_steps_per_mm = float(line.split("=")[1])
+                            state.y_steps_per_mm = y_steps_per_mm
+                            logger.info(f"Y steps per mm: {y_steps_per_mm}")
+                        elif line.startswith("$22="):
+                            # $22 reports if the homing cycle is enabled
+                            # returns 0 if disabled, 1 if enabled
+                            # Note: We only log this, we don't overwrite state.homing
+                            # because user preference (saved in state.json) should take precedence
+                            firmware_homing = int(line.split('=')[1])
+                            logger.info(f"Firmware homing setting ($22): {firmware_homing}, using user preference: {state.homing}")
+
+                # Check if we've received all the settings we need
+                if x_steps_per_mm is not None and y_steps_per_mm is not None:
+                    settings_complete = True
+            else:
+                # No data waiting, small sleep to prevent CPU thrashing
+                time.sleep(0.1)
 
-    if firmware_type == 'fluidnc':
-        if firmware_version:
-            logger.info(f"Detected FluidNC firmware, version: {firmware_version}")
-        else:
-            logger.info("Detected FluidNC firmware (version unknown)")
-        x_steps_per_mm, y_steps_per_mm = _get_steps_fluidnc()
+                # Retry every 3 seconds if no response received
+                if time.time() - last_retry_time > 3:
+                    logger.warning("No response yet, sending $$ command again")
+                    state.conn.send("$$\n")
+                    last_retry_time = time.time()
 
-        # Fallback to GRBL method if FluidNC queries failed
-        if x_steps_per_mm is None or y_steps_per_mm is None:
-            logger.warning("FluidNC setting queries failed, falling back to $$ command...")
-            x_steps_per_mm, y_steps_per_mm = _get_steps_grbl()
-    else:
-        if firmware_type == 'grbl':
-            if firmware_version:
-                logger.info(f"Detected GRBL firmware, version: {firmware_version}")
-            else:
-                logger.info("Detected GRBL firmware (version unknown)")
-        else:
-            logger.info("Could not detect firmware type, using GRBL commands")
-        x_steps_per_mm, y_steps_per_mm = _get_steps_grbl()
+        except Exception as e:
+            logger.error(f"Error getting machine steps: {e}")
+            time.sleep(0.5)
     
     # Process results and determine table type
-    settings_complete = (x_steps_per_mm is not None and y_steps_per_mm is not None)
     if settings_complete:
         if y_steps_per_mm == 180 and x_steps_per_mm == 256:
             state.table_type = 'dune_weaver_mini'
@@ -1019,38 +715,34 @@ def home(timeout=90):
                     homing_complete.set()
                     return
 
-                # Skip zeroing if X homed but Y failed - moving Y to 0 would crash it
-                # (Y controls rho/radial position which is unknown if Y didn't home)
-                if state.homed_x and not state.homed_y:
-                    logger.warning("Skipping position zeroing - X homed but Y failed (would crash Y axis)")
-                else:
-                    # Send x0 y0 to zero both positions using send_grbl_coordinates
-                    logger.info(f"Zeroing positions with x0 y0 f{homing_speed}")
-
-                    # Run async function in new event loop
-                    loop = asyncio.new_event_loop()
-                    asyncio.set_event_loop(loop)
-                    try:
-                        # Send G1 X0 Y0 F{homing_speed}
-                        result = loop.run_until_complete(send_grbl_coordinates(0, 0, homing_speed))
-                        if result == False:
-                            logger.error("Position zeroing failed - send_grbl_coordinates returned False")
-                            homing_complete.set()
-                            return
-                        logger.info("Position zeroing completed successfully")
-                    finally:
-                        loop.close()
-
-                    # Wait for device to reach idle state after zeroing movement
-                    logger.info("Waiting for device to reach idle state after zeroing...")
-                    idle_reached = check_idle()
+                # Send x0 y0 to zero both positions using send_grbl_coordinates
+                logger.info(f"Zeroing positions with x0 y0 f{homing_speed}")
 
-                    if not idle_reached:
-                        logger.error("Device did not reach idle state after zeroing")
+                # Run async function in new event loop
+                loop = asyncio.new_event_loop()
+                asyncio.set_event_loop(loop)
+                try:
+                    # Send G1 X0 Y0 F{homing_speed}
+                    result = loop.run_until_complete(send_grbl_coordinates(0, 0, homing_speed))
+                    if result == False:
+                        logger.error("Position zeroing failed - send_grbl_coordinates returned False")
                         homing_complete.set()
                         return
+                    logger.info("Position zeroing completed successfully")
+                finally:
+                    loop.close()
+
+                # Wait for device to reach idle state after zeroing movement
+                logger.info("Waiting for device to reach idle state after zeroing...")
+                idle_reached = check_idle()
+
+                if not idle_reached:
+                    logger.error("Device did not reach idle state after zeroing")
+                    homing_complete.set()
+                    return
 
                 # Set current position based on compass reference point (sensor mode only)
+                # Only set AFTER x0 y0 is confirmed and device is idle
                 offset_radians = math.radians(state.angular_homing_offset_degrees)
                 state.current_theta = offset_radians
                 state.current_rho = 0
@@ -1166,31 +858,12 @@ def check_idle():
             return True
         time.sleep(1)
 
-async def check_idle_async(timeout: float = 30.0):
+async def check_idle_async():
     """
     Continuously check if the device is idle (async version).
-
-    Args:
-        timeout: Maximum seconds to wait for idle state (default 30s)
-
-    Returns:
-        True if device became idle, False if timeout or stop requested
     """
     logger.info("Checking idle (async)")
-    start_time = asyncio.get_event_loop().time()
-
     while True:
-        # Check if stop was requested - exit early
-        if state.stop_requested:
-            logger.info("Stop requested during idle check, exiting early")
-            return False
-
-        # Check timeout
-        elapsed = asyncio.get_event_loop().time() - start_time
-        if elapsed > timeout:
-            logger.warning(f"Timeout ({timeout}s) waiting for device idle state")
-            return False
-
         response = await asyncio.to_thread(get_status_response)
         if response and "Idle" in response:
             logger.info("Device is idle")
@@ -1263,88 +936,6 @@ async def update_machine_position():
         except Exception as e:
             logger.error(f"Error updating machine position: {e}")
 
-
-def reset_work_coordinates():
-    """
-    Clear all work coordinate offsets for a clean start.
-
-    This ensures the work coordinate system starts fresh on each connection,
-    preventing accumulated offsets from previous sessions from affecting
-    pattern execution.
-
-    G92.1: Clears any G92 offset (resets work coordinates to machine coordinates)
-    G10 L2 P1 X0 Y0: Sets G54 work offset to 0 (for completeness)
-    """
-    if not state.conn or not state.conn.is_connected():
-        logger.warning("Cannot reset work coordinates: no active connection")
-        return False
-
-    try:
-        logger.info("Resetting work coordinate offsets")
-
-        # Clear any stale input data first
-        try:
-            while state.conn.in_waiting() > 0:
-                state.conn.readline()
-        except Exception:
-            pass
-
-        # Clear G92 offset
-        state.conn.send("G92.1\n")
-        time.sleep(0.2)
-
-        # Wait for 'ok' response
-        start_time = time.time()
-        got_ok = False
-        while time.time() - start_time < 2.0:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"G92.1 response: {response}")
-                    if response.lower() == "ok":
-                        got_ok = True
-                        break
-                    elif "error" in response.lower():
-                        logger.warning(f"G92.1 error: {response}")
-                        break
-            time.sleep(0.05)
-
-        if not got_ok:
-            logger.warning("Did not receive 'ok' for G92.1, continuing anyway")
-
-        # Set G54 offset to 0 (optional, for completeness)
-        state.conn.send("G10 L2 P1 X0 Y0\n")
-        time.sleep(0.2)
-
-        # Wait for 'ok' response
-        start_time = time.time()
-        got_ok = False
-        while time.time() - start_time < 2.0:
-            if state.conn.in_waiting() > 0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"G10 response: {response}")
-                    if response.lower() == "ok":
-                        got_ok = True
-                        break
-                    elif "error" in response.lower():
-                        logger.warning(f"G10 error: {response}")
-                        break
-            time.sleep(0.05)
-
-        if not got_ok:
-            logger.warning("Did not receive 'ok' for G10 L2 P1 X0 Y0, continuing anyway")
-
-        # Reset machine_x to 0 since work coordinates now start at 0
-        state.machine_x = 0.0
-        logger.info("Work coordinates reset complete")
-        return True
-
-    except Exception as e:
-        logger.error(f"Error resetting work coordinates: {e}")
-        return False
-
-
 def restart_connection(homing=False):
     """
     Restart the connection. If a connection exists, close it and attempt to establish a new one.

Fichier diff supprimé car celui-ci est trop grand
+ 268 - 683
modules/core/pattern_manager.py


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff