|
|
@@ -8,7 +8,7 @@ from datetime import datetime, time as datetime_time
|
|
|
from tqdm import tqdm
|
|
|
from modules.connection import connection_manager
|
|
|
from modules.core.state import state
|
|
|
-from math import pi
|
|
|
+from math import pi, isnan, isinf
|
|
|
import asyncio
|
|
|
import json
|
|
|
# Import for legacy support, but we'll use LED interface through state
|
|
|
@@ -477,6 +477,13 @@ class MotionControlThread:
|
|
|
|
|
|
def _move_polar_sync(self, theta: float, rho: float, speed: Optional[float] = None):
|
|
|
"""Synchronous version of move_polar for use in motion thread."""
|
|
|
+ # Check for valid machine position (can be None if homing failed)
|
|
|
+ if state.machine_x is None or state.machine_y is None:
|
|
|
+ logger.error("Cannot execute move: machine position unknown (homing may have failed)")
|
|
|
+ logger.error("Please home the machine before running patterns")
|
|
|
+ state.stop_requested = True
|
|
|
+ return
|
|
|
+
|
|
|
# This is the original sync logic but running in dedicated thread
|
|
|
if state.table_type == 'dune_weaver_mini':
|
|
|
x_scaling_factor = 2
|
|
|
@@ -506,6 +513,14 @@ class MotionControlThread:
|
|
|
# Use provided speed or fall back to state.speed
|
|
|
actual_speed = speed if speed is not None else state.speed
|
|
|
|
|
|
+ # Validate coordinates before sending to prevent GRBL error:2
|
|
|
+ if isnan(new_x_abs) or isnan(new_y_abs) or isinf(new_x_abs) or isinf(new_y_abs):
|
|
|
+ logger.error(f"Motion thread: Invalid coordinates detected - X:{new_x_abs}, Y:{new_y_abs}")
|
|
|
+ logger.error(f" theta:{theta}, rho:{rho}, current_theta:{state.current_theta}, current_rho:{state.current_rho}")
|
|
|
+ logger.error(f" x_steps_per_mm:{state.x_steps_per_mm}, y_steps_per_mm:{state.y_steps_per_mm}, gear_ratio:{state.gear_ratio}")
|
|
|
+ state.stop_requested = True
|
|
|
+ return
|
|
|
+
|
|
|
# Call sync version of send_grbl_coordinates in this thread
|
|
|
self._send_grbl_coordinates_sync(round(new_x_abs, 3), round(new_y_abs, 3), actual_speed)
|
|
|
|
|
|
@@ -518,10 +533,12 @@ class MotionControlThread:
|
|
|
def _send_grbl_coordinates_sync(self, x: float, y: float, speed: int = 600, timeout: int = 2, home: bool = False):
|
|
|
"""Synchronous version of send_grbl_coordinates for motion thread.
|
|
|
|
|
|
- Waits indefinitely for 'ok' because GRBL only responds after the move completes,
|
|
|
- which can take many seconds at slow speeds.
|
|
|
+ Waits for 'ok' with a timeout. GRBL sends 'ok' after the move completes,
|
|
|
+ which can take many seconds at slow speeds. We use a generous timeout
|
|
|
+ (120 seconds) to handle slow movements, but prevent indefinite hangs.
|
|
|
"""
|
|
|
gcode = f"$J=G91 G21 Y{y} F{speed}" if home else f"G1 G53 X{x} Y{y} F{speed}"
|
|
|
+ max_wait_time = 120 # Maximum seconds to wait for 'ok' response
|
|
|
|
|
|
while True:
|
|
|
# Check stop_requested at the start of each iteration
|
|
|
@@ -533,20 +550,52 @@ class MotionControlThread:
|
|
|
logger.debug(f"Motion thread sending G-code: {gcode}")
|
|
|
state.conn.send(gcode + "\n")
|
|
|
|
|
|
- # Wait indefinitely for 'ok' - GRBL sends it after move completes
|
|
|
+ # Wait for 'ok' with timeout
|
|
|
+ wait_start = time.time()
|
|
|
while True:
|
|
|
# Check stop_requested while waiting
|
|
|
if state.stop_requested:
|
|
|
logger.debug("Motion thread: Stop requested while waiting for response")
|
|
|
return False
|
|
|
+
|
|
|
+ # Check for timeout
|
|
|
+ elapsed = time.time() - wait_start
|
|
|
+ if elapsed > max_wait_time:
|
|
|
+ logger.error(f"Motion thread: Timeout ({max_wait_time}s) waiting for 'ok' response")
|
|
|
+ logger.error("Possible serial communication issue - stopping pattern")
|
|
|
+ state.stop_requested = True
|
|
|
+ return False
|
|
|
+
|
|
|
response = state.conn.readline()
|
|
|
if response:
|
|
|
logger.debug(f"Motion thread response: {response}")
|
|
|
if response.lower() == "ok":
|
|
|
logger.debug("Motion thread: Command execution confirmed.")
|
|
|
return True
|
|
|
- # Small sleep to prevent CPU spin when readline() times out
|
|
|
- time.sleep(0.01)
|
|
|
+ # Handle GRBL errors - these mean command failed, stop pattern
|
|
|
+ if response.lower().startswith("error"):
|
|
|
+ logger.error(f"Motion thread: GRBL error received: {response}")
|
|
|
+ logger.error(f"Failed command: {gcode}")
|
|
|
+ logger.error("Stopping pattern due to GRBL error")
|
|
|
+ state.stop_requested = True
|
|
|
+ return False
|
|
|
+ # Handle GRBL alarms - machine needs attention
|
|
|
+ if "alarm" in response.lower():
|
|
|
+ logger.error(f"Motion thread: GRBL ALARM: {response}")
|
|
|
+ logger.error("Machine alarm triggered - stopping pattern")
|
|
|
+ state.stop_requested = True
|
|
|
+ return False
|
|
|
+ # FluidNC may echo commands back before sending 'ok'
|
|
|
+ # Silently ignore echoed G-code commands (G0, G1, $J, etc.)
|
|
|
+ if response.startswith(('G0', 'G1', 'G2', 'G3', '$J', 'M')):
|
|
|
+ logger.debug(f"Motion thread: Ignoring echoed command: {response}")
|
|
|
+ continue # Read next line to get 'ok'
|
|
|
+ # Log truly unexpected responses
|
|
|
+ logger.warning(f"Motion thread: Unexpected response: '{response}'")
|
|
|
+ else:
|
|
|
+ # Log periodically when waiting for response (every 30s)
|
|
|
+ if int(elapsed) > 0 and int(elapsed) % 30 == 0 and elapsed - int(elapsed) < 0.1:
|
|
|
+ logger.warning(f"Motion thread: Still waiting for 'ok' after {int(elapsed)}s for command: {gcode}")
|
|
|
|
|
|
except Exception as e:
|
|
|
error_str = str(e)
|
|
|
@@ -1354,6 +1403,10 @@ async def stop_actions(clear_playlist = True, wait_for_lock = True):
|
|
|
if progress_update_task and not progress_update_task.done():
|
|
|
progress_update_task.cancel()
|
|
|
|
|
|
+ # Cancel the playlist task itself (late import to avoid circular dependency)
|
|
|
+ from modules.core import playlist_manager
|
|
|
+ await playlist_manager.cancel_current_playlist()
|
|
|
+
|
|
|
state.pause_condition.notify_all()
|
|
|
|
|
|
# Also set the pause event to wake up any paused patterns
|