|
@@ -16,8 +16,9 @@ logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
|
|
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:
|
|
async def _check_table_is_idle() -> bool:
|
|
@@ -94,12 +95,6 @@ class SerialConnection(BaseConnection):
|
|
|
with self.lock:
|
|
with self.lock:
|
|
|
return self.ser.in_waiting
|
|
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:
|
|
def is_connected(self) -> bool:
|
|
|
return self.ser is not None and self.ser.is_open
|
|
return self.ser is not None and self.ser.is_open
|
|
|
|
|
|
|
@@ -125,6 +120,8 @@ class SerialConnection(BaseConnection):
|
|
|
with self.lock:
|
|
with self.lock:
|
|
|
if self.ser.is_open:
|
|
if self.ser.is_open:
|
|
|
self.ser.close()
|
|
self.ser.close()
|
|
|
|
|
+ # Release the lock resources
|
|
|
|
|
+ self.lock = None
|
|
|
|
|
|
|
|
###############################################################################
|
|
###############################################################################
|
|
|
# WebSocket Connection Implementation
|
|
# WebSocket Connection Implementation
|
|
@@ -188,7 +185,9 @@ class WebSocketConnection(BaseConnection):
|
|
|
with self.lock:
|
|
with self.lock:
|
|
|
if self.ws:
|
|
if self.ws:
|
|
|
self.ws.close()
|
|
self.ws.close()
|
|
|
-
|
|
|
|
|
|
|
+ # Release the lock resources
|
|
|
|
|
+ self.lock = None
|
|
|
|
|
+
|
|
|
def list_serial_ports():
|
|
def list_serial_ports():
|
|
|
"""Return a list of available serial ports."""
|
|
"""Return a list of available serial ports."""
|
|
|
ports = serial.tools.list_ports.comports()
|
|
ports = serial.tools.list_ports.comports()
|
|
@@ -200,7 +199,7 @@ def device_init(homing=True):
|
|
|
try:
|
|
try:
|
|
|
if get_machine_steps():
|
|
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}")
|
|
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")
|
|
logger.fatal("Failed to get machine steps")
|
|
|
state.conn.close()
|
|
state.conn.close()
|
|
|
return False
|
|
return False
|
|
@@ -209,10 +208,6 @@ def device_init(homing=True):
|
|
|
state.conn.close()
|
|
state.conn.close()
|
|
|
return False
|
|
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()
|
|
machine_x, machine_y = get_machine_position()
|
|
|
if machine_x != state.machine_x or machine_y != state.machine_y:
|
|
if machine_x != state.machine_x or machine_y != state.machine_y:
|
|
|
logger.info(f'x, y; {machine_x}, {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}')
|
|
logger.info(f'State x, y; {state.machine_x}, {state.machine_y}')
|
|
|
|
|
|
|
|
time.sleep(2) # Allow time for the connection to establish
|
|
time.sleep(2) # Allow time for the connection to establish
|
|
|
- return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def connect_device(homing=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():
|
|
if state.conn is None or not state.conn.is_connected():
|
|
|
logger.warning("Cannot get status response: no active connection")
|
|
logger.warning("Cannot get status response: no active connection")
|
|
|
return False
|
|
return False
|
|
|
-
|
|
|
|
|
while True:
|
|
while True:
|
|
|
try:
|
|
try:
|
|
|
state.conn.send('?')
|
|
state.conn.send('?')
|
|
@@ -445,66 +438,32 @@ def parse_machine_position(response: str):
|
|
|
return None
|
|
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.
|
|
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}")
|
|
logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
|
|
|
|
|
|
|
|
|
|
+ # Track overall attempt time
|
|
|
overall_start_time = time.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:
|
|
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")
|
|
await asyncio.to_thread(state.conn.send, gcode + "\n")
|
|
|
logger.debug(f"Sent command: {gcode}")
|
|
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)
|
|
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:
|
|
except Exception as e:
|
|
|
|
|
+ # 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}")
|
|
|
|
|
|
|
@@ -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")
|
|
logger.info("Connection marked as disconnected due to device error")
|
|
|
return False
|
|
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:
|
|
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):
|
|
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.
|
|
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():
|
|
if not state.conn or not state.conn.is_connected():
|
|
|
logger.error("Cannot get machine steps: No connection available")
|
|
logger.error("Cannot get machine steps: No connection available")
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
+ x_steps_per_mm = None
|
|
|
|
|
+ y_steps_per_mm = None
|
|
|
|
|
+ start_time = time.time()
|
|
|
|
|
+
|
|
|
# Clear any pending data in the buffer
|
|
# Clear any pending data in the buffer
|
|
|
try:
|
|
try:
|
|
|
while state.conn.in_waiting() > 0:
|
|
while state.conn.in_waiting() > 0:
|
|
@@ -825,62 +519,64 @@ def get_machine_steps(timeout=10):
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.warning(f"Error clearing buffer: {e}")
|
|
logger.warning(f"Error clearing buffer: {e}")
|
|
|
|
|
|
|
|
- # Verify controller is responsive before querying
|
|
|
|
|
|
|
+ # Send the command to request all settings
|
|
|
try:
|
|
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:
|
|
if state.conn.in_waiting() > 0:
|
|
|
response = state.conn.readline()
|
|
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
|
|
# 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 settings_complete:
|
|
|
if y_steps_per_mm == 180 and x_steps_per_mm == 256:
|
|
if y_steps_per_mm == 180 and x_steps_per_mm == 256:
|
|
|
state.table_type = 'dune_weaver_mini'
|
|
state.table_type = 'dune_weaver_mini'
|
|
@@ -1019,38 +715,34 @@ def home(timeout=90):
|
|
|
homing_complete.set()
|
|
homing_complete.set()
|
|
|
return
|
|
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()
|
|
homing_complete.set()
|
|
|
return
|
|
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)
|
|
# 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)
|
|
offset_radians = math.radians(state.angular_homing_offset_degrees)
|
|
|
state.current_theta = offset_radians
|
|
state.current_theta = offset_radians
|
|
|
state.current_rho = 0
|
|
state.current_rho = 0
|
|
@@ -1166,31 +858,12 @@ def check_idle():
|
|
|
return True
|
|
return True
|
|
|
time.sleep(1)
|
|
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).
|
|
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)")
|
|
logger.info("Checking idle (async)")
|
|
|
- start_time = asyncio.get_event_loop().time()
|
|
|
|
|
-
|
|
|
|
|
while True:
|
|
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)
|
|
response = await asyncio.to_thread(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")
|
|
@@ -1263,88 +936,6 @@ async def update_machine_position():
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"Error updating machine position: {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):
|
|
def restart_connection(homing=False):
|
|
|
"""
|
|
"""
|
|
|
Restart the connection. If a connection exists, close it and attempt to establish a new one.
|
|
Restart the connection. If a connection exists, close it and attempt to establish a new one.
|