Просмотр исходного кода

Use $Bye for hard reset instead of G92 X0 Y0

G92 only sets work coordinate offset without changing MPos, causing
position queries to return stale values. $Bye performs a full FluidNC
soft reset which properly clears position counters to 0.

- Add perform_soft_reset() in connection_manager with proper wait logic
- Wait for "Grbl" startup banner (5s timeout) before proceeding
- Send $X unlock after reset in case of alarm state
- Simplify reset_theta() to use shared soft reset function
- Update /soft_reset endpoint to use new function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 неделя назад
Родитель
Сommit
f3449a8601
3 измененных файлов с 97 добавлено и 48 удалено
  1. 4 17
      main.py
  2. 87 0
      modules/connection/connection_manager.py
  3. 6 31
      modules/core/pattern_manager.py

+ 4 - 17
main.py

@@ -1784,7 +1784,7 @@ async def force_stop():
 
 @app.post("/soft_reset")
 async def soft_reset():
-    """Send Ctrl+X soft reset to the controller (DLC32/ESP32). Requires re-homing after."""
+    """Send $Bye soft reset to FluidNC controller. Resets position counters to 0."""
     if not (state.conn and state.conn.is_connected()):
         logger.warning("Attempted to soft reset without a connection")
         raise HTTPException(status_code=400, detail="Connection not established")
@@ -1793,23 +1793,10 @@ async def soft_reset():
         # Stop any running patterns first
         await pattern_manager.stop_actions()
 
-        # Access the underlying serial object directly for more reliable reset
-        # This bypasses the connection abstraction which may have buffering issues
-        from modules.connection.connection_manager import SerialConnection
-        if isinstance(state.conn, SerialConnection) and state.conn.ser:
-            state.conn.ser.reset_input_buffer()  # Clear any pending data
-            state.conn.ser.write(b'\x18')  # Ctrl+X as bytes
-            state.conn.ser.flush()
-            logger.info(f"Soft reset command (Ctrl+X) sent directly via serial to {state.port}")
-        else:
-            # Fallback for WebSocket or other connection types
-            state.conn.send('\x18')
-            logger.info("Soft reset command (Ctrl+X) sent via connection abstraction")
-
-        # Mark as needing homing since position is now unknown
-        state.is_homed = False
+        # Use the shared soft reset function
+        await connection_manager.perform_soft_reset()
 
-        return {"success": True, "message": "Soft reset sent. Homing required."}
+        return {"success": True, "message": "Soft reset sent. Position reset to 0."}
     except Exception as e:
         logger.error(f"Error sending soft reset: {e}")
         raise HTTPException(status_code=500, detail=str(e))

+ 87 - 0
modules/connection/connection_manager.py

@@ -1264,6 +1264,93 @@ async def update_machine_position():
             logger.error(f"Error updating machine position: {e}")
 
 
+async def perform_soft_reset():
+    """
+    Send $Bye soft reset to FluidNC controller and reset position counters.
+
+    $Bye triggers a software reset in FluidNC which clears all position counters
+    to 0. This is more reliable than G92 which only sets a work coordinate offset
+    without changing the actual machine position (MPos).
+    """
+    if not state.conn or not state.conn.is_connected():
+        logger.warning("Cannot perform soft reset: no active connection")
+        return False
+
+    try:
+        logger.info(f"Sending $Bye soft reset (was: X={state.machine_x:.2f}, Y={state.machine_y:.2f})")
+
+        # Clear any pending data first
+        if isinstance(state.conn, SerialConnection) and state.conn.ser:
+            state.conn.ser.reset_input_buffer()
+            state.conn.ser.write(b'$Bye\n')
+            state.conn.ser.flush()
+            logger.info(f"$Bye sent directly via serial to {state.port}")
+        else:
+            state.conn.send('$Bye\n')
+            logger.info("$Bye sent via connection abstraction")
+
+        # Wait for controller to fully restart
+        # The restart sequence is:
+        # 1. [MSG:INFO: Restarting] + ok
+        # 2. Many [MSG:INFO: ...] initialization lines
+        # 3. Final: "Grbl 3.9 [FluidNC v3.9.5 ...]" - this means ready
+        start_time = time.time()
+        reset_confirmed = False
+        while time.time() - start_time < 5.0:  # 5 second timeout for full reboot
+            try:
+                response = state.conn.readline()
+                if response:
+                    logger.debug(f"$Bye response: {response}")
+                    # Wait for the final "Grbl" startup banner - this means fully ready
+                    if response.startswith("Grbl") or "fluidnc" in response.lower():
+                        reset_confirmed = True
+                        logger.info(f"Controller restart complete: {response}")
+                        break
+            except Exception:
+                pass
+            await asyncio.sleep(0.05)
+
+        # Small delay to let controller fully stabilize
+        await asyncio.sleep(0.2)
+
+        # Unlock controller in case it's in alarm state after reset
+        if reset_confirmed:
+            logger.info("Sending $X to unlock controller after reset")
+            state.conn.send("$X\n")
+            # Wait for ok response
+            unlock_start = time.time()
+            while time.time() - unlock_start < 1.0:
+                try:
+                    response = state.conn.readline()
+                    if response:
+                        logger.debug(f"$X response: {response}")
+                        if response.lower() == "ok":
+                            logger.info("Controller unlocked")
+                            break
+                except Exception:
+                    pass
+                await asyncio.sleep(0.05)
+
+        # Reset state positions to 0 after soft reset
+        state.machine_x = 0.0
+        state.machine_y = 0.0
+
+        if reset_confirmed:
+            logger.info("Machine position reset to 0 via $Bye soft reset")
+        else:
+            logger.warning("$Bye sent but no reset confirmation received, position set to 0 anyway")
+
+        # Save the reset position
+        await asyncio.to_thread(state.save)
+        logger.info(f"Machine position saved: {state.machine_x}, {state.machine_y}")
+
+        return True
+
+    except Exception as e:
+        logger.error(f"Error performing soft reset: {e}")
+        return False
+
+
 def reset_work_coordinates():
     """
     Clear all work coordinate offsets for a clean start.

+ 6 - 31
modules/core/pattern_manager.py

@@ -1672,42 +1672,17 @@ def resume_execution():
     
 async def reset_theta():
     """
-    Reset theta to [0, 2π) range and reset work X and Y coordinates.
+    Reset theta to [0, 2π) range and hard reset machine position using $Bye.
 
-    G92 X0 Y0 sets current work position to X=0 Y=0 without moving.
-    This keeps coordinates bounded and prevents soft limit errors.
-    The soft limits check against MPos (machine position), which doesn't
-    change with G92, so this is safe for the hardware.
+    $Bye sends a soft reset to FluidNC which resets the controller and clears
+    all position counters to 0. This is more reliable than G92 which only sets
+    a work coordinate offset without changing the actual machine position (MPos).
     """
     logger.info('Resetting Theta')
     state.current_theta = state.current_theta % (2 * pi)
 
-    # Reset work X and Y coordinates to prevent accumulation
-    if state.conn and state.conn.is_connected():
-        try:
-            logger.info(f"Resetting work position (was: X={state.machine_x:.2f}, Y={state.machine_y:.2f})")
-            state.conn.send("G92 X0 Y0\n")
-
-            # Wait for ok response
-            start_time = time.time()
-            while time.time() - start_time < 2.0:
-                response = state.conn.readline()
-                if response:
-                    logger.debug(f"G92 X0 Y0 response: {response}")
-                    if response.lower() == "ok":
-                        state.machine_x = 0.0
-                        state.machine_y = 0.0
-                        logger.info("Work X and Y position reset to 0")
-                        break
-                    elif "error" in response.lower():
-                        logger.error(f"G92 X0 Y0 error: {response}")
-                        break
-                await asyncio.sleep(0.05)
-        except Exception as e:
-            logger.error(f"Error resetting work position: {e}")
-
-    # Call async function directly since we're in async context
-    await connection_manager.update_machine_position()
+    # Hard reset machine position using $Bye via connection_manager
+    await connection_manager.perform_soft_reset()
 
 def set_speed(new_speed):
     state.speed = new_speed