1
0
Эх сурвалжийг харах

create state manager, improve retry logic, optimize speed and acceleration

Tuan Nguyen 1 жил өмнө
parent
commit
2494e787c0

+ 1 - 1
.gitignore

@@ -4,4 +4,4 @@ __pycache__/
 *.pyo
 .env
 .idea
-playlists.json
+*.json

+ 7 - 4
dune_weaver_flask/app.py

@@ -7,6 +7,8 @@ from .modules.serial import serial_manager
 from dune_weaver_flask.modules.core import pattern_manager
 from dune_weaver_flask.modules.core import playlist_manager
 from .modules.firmware import firmware_manager
+from dune_weaver_flask.modules.core.state import state
+
 
 # Configure logging
 logging.basicConfig(
@@ -388,7 +390,7 @@ def set_speed():
         if not isinstance(new_speed, (int, float)) or new_speed <= 0:
             logger.warning(f"Invalid speed value received: {new_speed}")
             return jsonify({"success": False, "error": "Invalid speed value"}), 400
-        pattern_manager.set_speed(new_speed)
+        state.speed = new_speed
         return jsonify({"success": True, "speed": new_speed})
     except Exception as e:
         logger.error(f"Failed to set speed: {str(e)}")
@@ -457,12 +459,13 @@ def update_software():
 def on_exit():
     """Function to execute on application shutdown."""
     pattern_manager.stop_actions()
-
-# Register the on_exit function
-atexit.register(on_exit)
+    state.save()
 
 def entrypoint():
     logger.info("Starting Dune Weaver application...")
+    
+    # Register the on_exit function
+    atexit.register(on_exit)
     # Auto-connect to serial
     try:
         serial_manager.connect_to_serial()

+ 78 - 83
dune_weaver_flask/modules/core/pattern_manager.py

@@ -6,6 +6,7 @@ import logging
 from datetime import datetime
 from tqdm import tqdm
 from dune_weaver_flask.modules.serial import serial_manager
+from dune_weaver_flask.modules.core.state import state
 from math import pi
 
 # Configure logging
@@ -20,18 +21,6 @@ CLEAR_PATTERNS = {
 }
 os.makedirs(THETA_RHO_DIR, exist_ok=True)
 
-# Execution state
-stop_requested = False
-pause_requested = False
-pause_condition = threading.Condition()
-current_playing_file = None
-execution_progress = None
-current_playing_index = None
-current_playlist = None
-is_clearing = False
-current_theta = current_rho = 0
-speed = 800
-
 def list_theta_rho_files():
     files = []
     for root, _, filenames in os.walk(THETA_RHO_DIR):
@@ -89,7 +78,6 @@ def get_clear_pattern_file(clear_pattern_mode, path=None):
 
 def schedule_checker(schedule_hours):
     """Pauses/resumes execution based on a given time range."""
-    global pause_requested
     if not schedule_hours:
         return
 
@@ -97,122 +85,130 @@ def schedule_checker(schedule_hours):
     now = datetime.now().time()
 
     if start_time <= now < end_time:
-        if pause_requested:
+        if state.pause_requested:
             logger.info("Starting execution: Within schedule")
-        pause_requested = False
-        with pause_condition:
-            pause_condition.notify_all()
+            serial_manager.update_machine_position()
+        state.pause_requested = False
+        with state.pause_condition:
+            state.pause_condition.notify_all()
     else:
-        if not pause_requested:
+        if not state.pause_requested:
             logger.info("Pausing execution: Outside schedule")
-        pause_requested = True
+        state.pause_requested = True
+        serial_manager.update_machine_position()
         threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
 
 def wait_for_start_time(schedule_hours):
     """Keep checking every 30 seconds if the time is within the schedule to resume execution."""
-    global pause_requested
     start_time, end_time = schedule_hours
 
-    while pause_requested:
+    while state.pause_requested:
         now = datetime.now().time()
         if start_time <= now < end_time:
             logger.info("Resuming execution: Within schedule")
-            pause_requested = False
-            with pause_condition:
-                pause_condition.notify_all()
+            state.pause_requested = False
+            with state.pause_condition:
+                state.pause_condition.notify_all()
             break
         else:
             time.sleep(30)
             
-def interpolate_path(theta, rho, speed=speed):
-    global current_theta, current_rho
-    delta_theta = current_theta - theta
-    delta_rho = current_rho - rho
-    x = (theta - current_theta)/(2*pi)*100
-    y = (rho-current_rho) * 100
-    offset = x/100 * 27.8260869565
-    y += offset
-    serial_manager.send_grbl_coordinates(x, y, speed)
-    current_theta = theta
-    current_rho = rho
+            
+def interpolate_path(theta, rho):
+    delta_theta = theta - state.current_theta
+    delta_rho = rho - state.current_rho
+    x_increment = delta_theta / (2 * pi) * 100
+    y_increment = delta_rho * 100/5
+    
+    offset = x_increment * (1600/5750/5) # Total angular steps = 16000 / gear ratio = 10 / angular steps = 5750
+    y_increment += offset
+    
+    new_x_abs = state.machine_x + x_increment
+    new_y_abs = state.machine_y + y_increment
+    
+    # dynamic_speed = compute_dynamic_speed(rho, max_speed=state.speed)
+    
+    serial_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), state.speed)
+    state.current_theta = theta
+    state.current_rho = rho
+    state.machine_x = new_x_abs
+    state.machine_y = new_y_abs
     
 def reset_theta():
     logger.info('Resetting Theta')
-    global current_theta
-    current_theta = 0
+    state.current_theta = 0
+    serial_manager.update_machine_position()
 
 def set_speed(new_speed):
-    global speed
-    speed = new_speed
+    state.speed = new_speed
+    logger.info(f'Set new state.speed {new_speed}')
 
 def run_theta_rho_file(file_path, schedule_hours=None):
     """Run a theta-rho file by sending data in optimized batches with tqdm ETA tracking."""
     if not file_path:
         return
-    global current_playing_file, execution_progress, stop_requested, current_theta, current_rho, speed
     coordinates = parse_theta_rho_file(file_path)
     total_coordinates = len(coordinates)
 
     if total_coordinates < 2:
         logger.warning("Not enough coordinates for interpolation")
-        current_playing_file = None
-        execution_progress = None
+        state.current_playing_file = None
+        state.execution_progress = None
         return
 
-    execution_progress = (0, total_coordinates, None)
+    state.execution_progress = (0, total_coordinates, None)
 
     stop_actions()
     BATCH_SIZE = 15  # Max planner buffer size
 
     with serial_manager.serial_lock:
-        current_playing_file = file_path
-        execution_progress = (0, 0, None)
-        stop_requested = False
+        state.current_playing_file = file_path
+        state.execution_progress = (0, 0, None)
+        state.stop_requested = False
         logger.info(f"Starting pattern execution: {file_path}")
-        logger.info(f"t: {current_theta}, r: {current_rho}")
+        logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
         reset_theta()
         for coordinate in tqdm(coordinates):
             theta, rho = coordinate
-            if stop_requested:
+            if state.stop_requested:
                 logger.info("Execution stopped by user after completing the current batch")
                 break
 
-            with pause_condition:
-                while pause_requested:
+            with state.pause_condition:
+                while state.pause_requested:
                     logger.info("Execution paused...")
-                    pause_condition.wait()
+                    state.pause_condition.wait()
 
             schedule_checker(schedule_hours)
-            interpolate_path(theta, rho, speed)
+            interpolate_path(theta, rho)
 
         serial_manager.check_idle()
 
-    current_playing_file = None
-    execution_progress = None
+    state.current_playing_file = None
+    state.execution_progress = None
     logger.info("Pattern execution completed")
 
 def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, schedule_hours=None):
     """Run multiple .thr files in sequence with options."""
-    global stop_requested, current_playlist, current_playing_index
-    stop_requested = False
+    state.stop_requested = False
     
     if shuffle:
         random.shuffle(file_paths)
         logger.info("Playlist shuffled")
 
-    current_playlist = file_paths
+    state.current_playlist = file_paths
 
     while True:
         for idx, path in enumerate(file_paths):
             logger.info(f"Upcoming pattern: {path}")
-            current_playing_index = idx
+            state.current_playing_index = idx
             schedule_checker(schedule_hours)
-            if stop_requested:
+            if state.stop_requested:
                 logger.info("Execution stopped before starting next pattern")
                 return
 
             if clear_pattern:
-                if stop_requested:
+                if state.stop_requested:
                     logger.info("Execution stopped before running the next clear pattern")
                     return
 
@@ -220,12 +216,12 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
                 logger.info(f"Running clear pattern: {clear_file_path}")
                 run_theta_rho_file(clear_file_path, schedule_hours)
 
-            if not stop_requested:
+            if not state.stop_requested:
                 logger.info(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
                 run_theta_rho_file(path, schedule_hours)
 
             if idx < len(file_paths) - 1:
-                if stop_requested:
+                if state.stop_requested:
                     logger.info("Execution stopped before running the next clear pattern")
                     return
                 if pause_time > 0:
@@ -248,32 +244,31 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
 
 def stop_actions():
     """Stop all current pattern execution."""
-    global pause_requested, stop_requested, current_playing_index, current_playlist, is_clearing, current_playing_file, execution_progress
-    with pause_condition:
-        pause_requested = False
-        stop_requested = True
-        current_playing_index = None
-        current_playlist = None
-        is_clearing = False
-        current_playing_file = None
-        execution_progress = None
+    with state.pause_condition:
+        state.pause_requested = False
+        state.stop_requested = True
+        state.current_playing_index = None
+        state.current_playlist = None
+        state.is_clearing = False
+        state.current_playing_file = None
+        state.execution_progress = None
+    serial_manager.update_machine_position()
 
 def get_status():
     """Get the current execution status."""
-    global is_clearing
-    # Update is_clearing based on current file
-    if current_playing_file in CLEAR_PATTERNS.values():
-        is_clearing = True
+    # Update state.is_clearing based on current file
+    if state.current_playing_file in CLEAR_PATTERNS.values():
+        state.is_clearing = True
     else:
-        is_clearing = False
+        state.is_clearing = False
 
     return {
         "ser_port": serial_manager.get_port(),
-        "stop_requested": stop_requested,
-        "pause_requested": pause_requested,
-        "current_playing_file": current_playing_file,
-        "execution_progress": execution_progress,
-        "current_playing_index": current_playing_index,
-        "current_playlist": current_playlist,
-        "is_clearing": is_clearing
+        "state.stop_requested": state.stop_requested,
+        "state.pause_requested": state.pause_requested,
+        "state.current_playing_file": state.current_playing_file,
+        "state.execution_progress": state.execution_progress,
+        "state.current_playing_index": state.current_playing_index,
+        "state.current_playlist": state.current_playlist,
+        "state.is_clearing": state.is_clearing
     }

+ 81 - 0
dune_weaver_flask/modules/core/state.py

@@ -0,0 +1,81 @@
+# state.py
+import threading
+import json
+import os
+
+STATE_FILE = "state.json"
+SPEED = 300
+
+class AppState:
+    def __init__(self):
+        # Execution state variables
+        self.stop_requested = False
+        self.pause_requested = False
+        self.pause_condition = threading.Condition()
+        self.current_playing_file = None
+        self.execution_progress = None
+        self.current_playing_index = None
+        self.current_playlist = None
+        self.is_clearing = False
+        self.current_theta = 0
+        self.current_rho = 0
+        self.speed = SPEED
+        
+        # Machine position variables
+        self.machine_x = 0.0
+        self.machine_y = 0.0
+        
+        self.load()
+
+    def to_dict(self):
+        """Return a dictionary representation of the state."""
+        return {
+            "stop_requested": self.stop_requested,
+            "pause_requested": self.pause_requested,
+            "current_playing_file": self.current_playing_file,
+            "execution_progress": self.execution_progress,
+            "current_playing_index": self.current_playing_index,
+            "current_playlist": self.current_playlist,
+            "is_clearing": self.is_clearing,
+            "current_theta": self.current_theta,
+            "current_rho": self.current_rho,
+            "speed": self.speed,
+            "machine_x": self.machine_x,
+            "machine_y": self.machine_y,
+        }
+
+    def from_dict(self, data):
+        """Update state from a dictionary."""
+        self.stop_requested = data.get("stop_requested", False)
+        self.pause_requested = data.get("pause_requested", False)
+        self.current_playing_file = data.get("current_playing_file")
+        self.execution_progress = data.get("execution_progress")
+        self.current_playing_index = data.get("current_playing_index")
+        self.current_playlist = data.get("current_playlist")
+        self.is_clearing = data.get("is_clearing", False)
+        self.current_theta = data.get("current_theta", 0)
+        self.current_rho = data.get("current_rho", 0)
+        self.speed = data.get("speed", 300)
+        self.machine_x = data.get("machine_x", 0.0)
+        self.machine_y = data.get("machine_y", 0.0)
+
+    def save(self):
+        """Save the current state to a JSON file."""
+        with open(STATE_FILE, "w") as f:
+            json.dump(self.to_dict(), f)
+
+    def load(self):
+        """Load state from a JSON file. If the file doesn't exist, create it with default values."""
+        if not os.path.exists(STATE_FILE):
+            # File doesn't exist: create one with the current (default) state.
+            self.save(STATE_FILE)
+            return
+        try:
+            with open(STATE_FILE, "r") as f:
+                data = json.load(f)
+            self.from_dict(data)
+        except Exception as e:
+            print(f"Error loading state from {STATE_FILE}: {e}")
+
+# Create a singleton instance that you can import elsewhere:
+state = AppState()

+ 119 - 62
dune_weaver_flask/modules/serial/serial_manager.py

@@ -3,11 +3,11 @@ import serial.tools.list_ports
 import threading
 import time
 import logging
+from dune_weaver_flask.modules.core.state import state
 
-# Configure logging
 logger = logging.getLogger(__name__)
 
-# Global state
+# Global variables
 ser = None
 ser_port = None
 serial_lock = threading.RLock()
@@ -18,6 +18,7 @@ arduino_table_name = None
 arduino_driver_type = 'Unknown'
 firmware_version = 'Unknown'
 
+
 def list_serial_ports():
     """Return a list of available serial ports."""
     ports = serial.tools.list_ports.comports()
@@ -25,13 +26,20 @@ def list_serial_ports():
     logger.debug(f"Available serial ports: {available_ports}")
     return available_ports
 
+
 def startup_gcodes():
-    ser.write(f"Report/Status=2".encode())
-    ser.flush()
     while True:
-        if ser.in_waiting > 0:
-            response = ser.readline().decode().strip()
-            logger.debug(f"Response: {response}")
+        with serial_lock:
+            ser.write("Report/Status=2".encode())
+            ser.flush()
+            while ser.in_waiting > 0:
+                response = ser.readline().decode().strip()
+                logger.debug(f"Response: {response}")
+                if "Report" in response:
+                    logger.info(response)
+                    return
+        time.sleep(1)
+
 
 def connect_to_serial(port=None, baudrate=115200):
     """Automatically connect to the first available serial port or a specified port."""
@@ -50,7 +58,14 @@ def connect_to_serial(port=None, baudrate=115200):
             ser = serial.Serial(port, baudrate, timeout=2)
             ser_port = port
         # startup_gcodes()
-        home()
+        machine_x, machine_y = get_machine_position()
+        if not machine_x or not machine_y or machine_x != state.machine_x or machine_y != state.machine_y:
+            logger.info(f'x, y; {machine_x}, {machine_y}')
+            logger.info(f'State x, y; {state.machine_x}, {state.machine_y}')
+            home()
+        else:
+            logger.info('Machine position known, skipping home')
+        
         logger.info(f"Connected to serial port: {port}")
         time.sleep(2)  # Allow time for the connection to establish
 
@@ -58,8 +73,6 @@ def connect_to_serial(port=None, baudrate=115200):
         while ser.in_waiting > 0:
             line = ser.readline().decode().strip()
             logger.debug(f"Arduino: {line}")
-
-            # Store the device details based on the expected messages
             if "Table:" in line:
                 arduino_table_name = line.replace("Table: ", "").strip()
             elif "Drivers:" in line:
@@ -69,7 +82,6 @@ def connect_to_serial(port=None, baudrate=115200):
 
         logger.info(f"Detected Table: {arduino_table_name or 'Unknown'}")
         logger.info(f"Detected Drivers: {arduino_driver_type or 'Unknown'}")
-
         return True
     except serial.SerialException as e:
         logger.error(f"Failed to connect to serial port {port}: {e}")
@@ -78,6 +90,7 @@ def connect_to_serial(port=None, baudrate=115200):
     logger.error("Max retries reached. Could not connect to a serial port.")
     return False
 
+
 def disconnect_serial():
     """Disconnect the current serial connection."""
     global ser, ser_port
@@ -87,31 +100,88 @@ def disconnect_serial():
         ser = None
     ser_port = None
 
+
 def restart_serial(port, baudrate=115200):
     """Restart the serial connection."""
     logger.info(f"Restarting serial connection on port {port}")
     disconnect_serial()
     return connect_to_serial(port, baudrate)
 
+
 def is_connected():
     """Check if serial connection is established and open."""
     return ser is not None and ser.is_open
 
+
 def get_port():
     """Get the current serial port."""
     return ser_port
 
 
-def send_grbl_coordinates(x, y, speed=600, timeout=2, retry_interval=1):
+def get_status_response():
     """
-    Send a G-code command to FluidNC and wait up to 2s for an 'ok' response.
-    If no 'ok' is received, retry every 1 second until successful.
+    Send a status query ('?') and return the response string if available.
+    This helper centralizes the query logic used throughout the module.
     """
-    logger.debug(f"Sending G-code: X{x}, Y{y} at F{speed}")
+    while True:
+        with serial_lock:
+            ser.write('?'.encode())
+            ser.flush()
+            while ser.in_waiting > 0:
+                response = ser.readline().decode().strip()
+                if "WPos" in response:
+                    logger.info(f"Status response: {response}")
+                    return response
+        time.sleep(1)
+
 
-    while True:  # Keep retrying indefinitely until 'ok' is received
+
+def parse_machine_position(response):
+    """
+    Parse the work position (WPos) from a status response.
+    Expected format: "<...|WPos:-994.869,-321.861,0.000|...>"
+    Returns a tuple (work_x, work_y) if found, else None.
+    """
+    if "WPos:" not in response:
+        return None
+    try:
+        wpos_section = next((part for part in response.split("|") if part.startswith("WPos:")), None)
+        if wpos_section:
+            wpos_str = wpos_section.split(":", 1)[1]
+            wpos_values = wpos_str.split(",")
+            work_x = float(wpos_values[0])
+            work_y = float(wpos_values[1])
+            return work_x, work_y
+    except Exception as e:
+        logger.error(f"Error parsing work position: {e}")
+    return None
+
+
+def parse_buffer_info(response):
+    """
+    Parse the planner and serial buffer info from a status response.
+    Expected format: "<...|Bf:15,128|...>"
+    Returns a dictionary with keys 'planner_buffer' and 'serial_buffer' if found, else None.
+    """
+    if "|Bf:" in response:
+        try:
+            buffer_section = response.split("|Bf:")[1].split("|")[0]
+            planner_buffer, serial_buffer = map(int, buffer_section.split(","))
+            return {"planner_buffer": planner_buffer, "serial_buffer": serial_buffer}
+        except ValueError:
+            logger.warning("Failed to parse buffer info from response")
+    return None
+
+
+def send_grbl_coordinates(x, y, speed=600, timeout=2, retry_interval=1):
+    """
+    Send a G-code command to FluidNC and wait up to timeout seconds for an 'ok' response.
+    If no 'ok' is received, retry every retry_interval seconds until successful.
+    """
+    logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
+    while True:
         with serial_lock:
-            gcode = f"$J=G91 G21 X{x:.3f} Y{y:.3f} F{speed}"
+            gcode = f"G1 G21 X{x} Y{y} F{speed}"
             ser.write(f"{gcode}\n".encode())
             ser.flush()
             logger.debug(f"Sent command: {gcode}")
@@ -125,55 +195,42 @@ def send_grbl_coordinates(x, y, speed=600, timeout=2, retry_interval=1):
                         logger.debug("Command execution confirmed.")
                         return  # Exit function when 'ok' is received
 
-            logger.warning(f"No 'ok' received for X{x}, Y{y}. Retrying in {retry_interval}s...")
-        
-        time.sleep(retry_interval)  # Wait before retrying
+            logger.warning(f"No 'ok' received for X{x} Y{y}. Retrying in {retry_interval}s...")
+
+        time.sleep(retry_interval)
+
 
 def home():
-    logger.info("Homing")
-    send_grbl_coordinates(0, -110, 1000)
-    current_theta = current_rho = 0
+    logger.info(f"Homing with speed {state.speed}")
+    send_grbl_coordinates(0, -110/5, state.speed)
+    state.current_theta = state.current_rho = 0
+    update_machine_position()
+
 
 def check_idle():
-    """Continuously check if the machine is in the 'Idle' state."""
+    """
+    Continuously check if the machine is in the 'Idle' state.
+    """
     logger.info("Checking idle")
     while True:
-        with serial_lock:
-            ser.write('?'.encode())  # Send status query
-            ser.flush()  # Ensure it's sent immediately
-
-            if ser.in_waiting > 0:
-                response = ser.readline().decode().strip()
-                logger.info(f"Response: {response}")
-                if "Idle" in response:
-                    logger.info("Tabble is idle")
-                    return True  # Exit function once 'Idle' is received
-
-        time.sleep(1)  # Wait before retrying
+        response = get_status_response()
+        if response and "Idle" in response:
+            logger.info("Table is idle")
+            update_machine_position()
+            return True  # Exit once 'Idle' is confirmed
+        time.sleep(1)
         
-def check_buffer():
-    """Check the available planner and serial buffer in FluidNC."""
-    logger.debug("Checking buffer availability")
-
-    with serial_lock:
-        ser.write('?'.encode())  # Send status query
-        ser.flush()  # Ensure it's sent immediately
-
-        if ser.in_waiting > 0:
-            response = ser.readline().decode().strip()
-            logger.debug(f"Response: {response}")
-
-            # Extract buffer values from the response (Format: <Idle|MPos:...|Bf:xx,yy|FS:...>)
-            buffer_info = None
-            if "|Bf:" in response:
-                try:
-                    buffer_section = response.split("|Bf:")[1].split("|")[0]
-                    planner_buffer, serial_buffer = map(int, buffer_section.split(","))
-                    buffer_info = {"planner_buffer": planner_buffer, "serial_buffer": serial_buffer}
-                except ValueError:
-                    logger.warning("Failed to parse buffer info from response")
-
-            logger.debug(f"Buffer Left: {buffer_info}")
-            return buffer_info
-
-    return None  # Return None if no buffer data is available
+def get_machine_position():
+    response = get_status_response()
+    logger.debug(response)
+    if response:
+        pos = parse_machine_position(response)
+        if pos:
+            machine_x, machine_y = pos
+            logger.debug(f"Machine position: X={machine_x}, Y={machine_y}")
+            return machine_x, machine_y
+    return None, None
+
+def update_machine_position():
+    state.machine_x, state.machine_y = get_machine_position()
+    state.save()

+ 9 - 67
firmware/dlc32_config.yaml

@@ -12,19 +12,10 @@ axes:
   shared_stepper_disable_pin: i2so.0
   x:
     steps_per_mm: 320
-    max_rate_mm_per_min: 10000
-    acceleration_mm_per_sec2: 1500
+    max_rate_mm_per_min: 900
+    acceleration_mm_per_sec2: 1000
     max_travel_mm: 325
     soft_limits: false
-    homing:
-      cycle: 1
-      positive_direction: false
-      mpos_mm: 0
-      feed_mm_per_min: 300
-      seek_mm_per_min: 5000
-      settle_ms: 500
-      seek_scaler: 1.1
-      feed_scaler: 1.1
     motor0:
       limit_neg_pin: gpio.36
       hard_limits: false
@@ -40,19 +31,10 @@ axes:
       limit_all_pin: NO_PIN
   y:
     steps_per_mm: 57.5
-    max_rate_mm_per_min: 12000
-    acceleration_mm_per_sec2: 1500
+    max_rate_mm_per_min: 3000
+    acceleration_mm_per_sec2: 1000
     max_travel_mm: 220
     soft_limits: false
-    homing:
-      cycle: 1
-      positive_direction: false
-      mpos_mm: 0
-      feed_mm_per_min: 300
-      seek_mm_per_min: 5000
-      settle_ms: 500
-      seek_scaler: 1.1
-      feed_scaler: 1.1
     motor0:
       limit_neg_pin: gpio.35
       hard_limits: false
@@ -66,42 +48,10 @@ axes:
         ms3_pin: NO_PIN
       limit_pos_pin: NO_PIN
       limit_all_pin: NO_PIN
-  z:
-    steps_per_mm: 157.75
-    max_rate_mm_per_min: 12000
-    acceleration_mm_per_sec2: 500
-    max_travel_mm: 80
-    soft_limits: true
-    homing:
-      cycle: 0
-      positive_direction: false
-      mpos_mm: 0
-      feed_mm_per_min: 300
-      seek_mm_per_min: 1000
-      settle_ms: 500
-      seek_scaler: 1.1
-      feed_scaler: 1.1
-    motor0:
-      limit_neg_pin: gpio.34
-      hard_limits: false
-      pulloff_mm: 1
-      stepstick:
-        step_pin: i2so.3
-        direction_pin: i2so.4
-        disable_pin: NO_PIN
-        ms1_pin: NO_PIN
-        ms2_pin: NO_PIN
-        ms3_pin: NO_PIN
-      limit_pos_pin: NO_PIN
-      limit_all_pin: NO_PIN
 i2so:
   bck_pin: gpio.16
   data_pin: gpio.21
   ws_pin: gpio.17
-spi:
-  miso_pin: gpio.12
-  mosi_pin: gpio.13
-  sck_pin: gpio.14
 sdcard:
   cs_pin: gpio.15
   card_detect_pin: NO_PIN
@@ -117,23 +67,11 @@ control:
   fault_pin: NO_PIN
   estop_pin: NO_PIN
 macros:
-  macro0: G91
+  macro0: G90
 coolant:
   flood_pin: NO_PIN
   mist_pin: NO_PIN
   delay_ms: 0
-probe:
-  pin: gpio.22
-  check_mode_start: true
-  toolsetter_pin: NO_PIN
-Laser:
-  pwm_hz: 5000
-  output_pin: gpio.32
-  enable_pin: i2so.7
-  disable_with_s0: false
-  s0_with_disable: false
-  tool_num: 0
-  speed_map: 0=0.000% 0=12.500% 1700=100.000%
 user_outputs:
   analog0_pin: NO_PIN
   analog1_pin: NO_PIN
@@ -149,3 +87,7 @@ user_outputs:
   digital3_pin: NO_PIN
 start:
   must_home: false
+spi:
+  miso_pin: gpio.12
+  mosi_pin: gpio.13
+  sck_pin: gpio.14