فهرست منبع

add new homing mode

tuanchris 3 ماه پیش
والد
کامیت
867d76cfbb
7فایلهای تغییر یافته به همراه271 افزوده شده و 576 حذف شده
  1. 18 42
      main.py
  2. 117 95
      modules/connection/connection_manager.py
  3. 0 112
      modules/connection/reed_switch.py
  4. 10 19
      modules/core/state.py
  5. 72 85
      static/js/settings.js
  6. 54 68
      templates/settings.html
  7. 0 155
      test_reed_switch.py

+ 18 - 42
main.py

@@ -95,12 +95,6 @@ async def lifespan(app: FastAPI):
     # Startup
     logger.info("Starting Dune Weaver application...")
 
-    # Check for advanced calibration feature flag (Desert Compass)
-    advanced_cal_env = os.getenv('ADVANCED_CALIBRATION', 'false').lower()
-    state.advanced_calibration_enabled = advanced_cal_env in ('true', '1', 'yes')
-    if state.advanced_calibration_enabled:
-        logger.info("Advanced calibration features enabled (Desert Compass)")
-
     # Register signal handlers
     signal.signal(signal.SIGINT, signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
@@ -500,56 +494,38 @@ async def set_scheduled_pause(request: ScheduledPauseRequest):
         logger.error(f"Error updating Still Sands settings: {str(e)}")
         raise HTTPException(status_code=500, detail=f"Failed to update Still Sands settings: {str(e)}")
 
-@app.get("/api/advanced-calibration-status")
-async def get_advanced_calibration_status():
-    """Check if advanced calibration features (Desert Compass) are enabled."""
-    return {
-        "enabled": state.advanced_calibration_enabled
-    }
-
-@app.get("/api/angular-homing")
-async def get_angular_homing():
-    """Get current Desert Compass settings."""
-    if not state.advanced_calibration_enabled:
-        raise HTTPException(status_code=403, detail="Advanced calibration features not enabled")
-
+@app.get("/api/homing-config")
+async def get_homing_config():
+    """Get homing configuration (mode and compass offset)."""
     return {
-        "angular_homing_enabled": state.angular_homing_enabled,
-        "angular_homing_gpio_pin": state.angular_homing_gpio_pin,
-        "angular_homing_invert_state": state.angular_homing_invert_state,
+        "homing_mode": state.homing,
         "angular_homing_offset_degrees": state.angular_homing_offset_degrees
     }
 
-class AngularHomingRequest(BaseModel):
-    angular_homing_enabled: bool
-    angular_homing_gpio_pin: int = 18
-    angular_homing_invert_state: bool = False
+class HomingConfigRequest(BaseModel):
+    homing_mode: int = 0  # 0 = crash, 1 = sensor
     angular_homing_offset_degrees: float = 0.0
 
-@app.post("/api/angular-homing")
-async def set_angular_homing(request: AngularHomingRequest):
-    """Update Desert Compass settings."""
-    if not state.advanced_calibration_enabled:
-        raise HTTPException(status_code=403, detail="Advanced calibration features not enabled")
-
+@app.post("/api/homing-config")
+async def set_homing_config(request: HomingConfigRequest):
+    """Set homing configuration (mode and compass offset)."""
     try:
-        # Validate GPIO pin
-        if request.angular_homing_gpio_pin < 2 or request.angular_homing_gpio_pin > 27:
-            raise HTTPException(status_code=400, detail="GPIO pin must be between 2 and 27")
+        # Validate homing mode
+        if request.homing_mode not in [0, 1]:
+            raise HTTPException(status_code=400, detail="Homing mode must be 0 (crash) or 1 (sensor)")
 
-        state.angular_homing_enabled = request.angular_homing_enabled
-        state.angular_homing_gpio_pin = request.angular_homing_gpio_pin
-        state.angular_homing_invert_state = request.angular_homing_invert_state
+        state.homing = request.homing_mode
         state.angular_homing_offset_degrees = request.angular_homing_offset_degrees
         state.save()
 
-        logger.info(f"Desert Compass {'enabled' if request.angular_homing_enabled else 'disabled'}, GPIO pin: {request.angular_homing_gpio_pin}, invert: {request.angular_homing_invert_state}, offset: {request.angular_homing_offset_degrees}°")
-        return {"success": True, "message": "Desert Compass settings updated"}
+        mode_name = "crash" if request.homing_mode == 0 else "sensor"
+        logger.info(f"Homing mode set to {mode_name}, compass offset set to {request.angular_homing_offset_degrees}°")
+        return {"success": True, "message": "Homing configuration updated"}
     except HTTPException:
         raise
     except Exception as e:
-        logger.error(f"Error updating Desert Compass settings: {str(e)}")
-        raise HTTPException(status_code=500, detail=f"Failed to update Desert Compass settings: {str(e)}")
+        logger.error(f"Error updating homing configuration: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"Failed to update homing configuration: {str(e)}")
 
 @app.get("/list_serial_ports")
 async def list_ports():

+ 117 - 95
modules/connection/connection_manager.py

@@ -10,7 +10,6 @@ from modules.core import pattern_manager
 from modules.core.state import state
 from modules.led.led_interface import LEDInterface
 from modules.led.idle_timeout_manager import idle_timeout_manager
-from modules.connection.reed_switch import ReedSwitchMonitor
 
 logger = logging.getLogger(__name__)
 
@@ -519,12 +518,23 @@ def get_machine_steps(timeout=10):
 
 def home(timeout=90):
     """
-    Perform homing by checking device configuration and sending the appropriate commands.
+    Perform homing sequence based on configured mode:
+
+    Mode 0 (Crash):
+        - Y axis moves -22mm (or -30mm for mini) until physical stop
+        - Set theta=0, rho=0 (no x0 y0 command)
+
+    Mode 1 (Sensor):
+        - Send $H command to home both X and Y axes
+        - Wait for [MSG:Homed:X] and [MSG:Homed:Y] messages
+        - Send x0 y0 to zero positions
+        - Set theta to compass offset, rho=0
 
     Args:
-        timeout: Maximum time in seconds to wait for homing to complete (default: 60)
+        timeout: Maximum time in seconds to wait for homing to complete (default: 90)
     """
     import threading
+    import math
 
     # Check for alarm state before homing and unlock if needed
     if not check_and_unlock_alarm():
@@ -538,133 +548,145 @@ def home(timeout=90):
     def home_internal():
         nonlocal homing_success
         try:
-            if state.homing:
-                logger.info("Using sensorless homing")
+            if state.homing == 1:
+                # Mode 1: Sensor-based homing using $H
+                logger.info("Using sensor-based homing mode ($H)")
+
+                # Clear any pending responses
+                state.homed_x = False
+                state.homed_y = False
+
+                # Send $H command
                 state.conn.send("$H\n")
-                state.conn.send("G1 Y0 F100\n")
+                logger.info("Sent $H command, waiting for homing messages...")
+
+                # Wait for [MSG:Homed:X] and [MSG:Homed:Y] messages
+                max_wait_time = 30  # 30 seconds timeout for homing messages
+                start_time = time.time()
+
+                while (time.time() - start_time) < max_wait_time:
+                    try:
+                        response = state.conn.readline()
+                        if response:
+                            logger.debug(f"Homing response: {response}")
+
+                            # Check for homing messages
+                            if "[MSG:Homed:X]" in response:
+                                state.homed_x = True
+                                logger.info("Received [MSG:Homed:X]")
+                            if "[MSG:Homed:Y]" in response:
+                                state.homed_y = True
+                                logger.info("Received [MSG:Homed:Y]")
+
+                            # Break if we've received both messages
+                            if state.homed_x and state.homed_y:
+                                logger.info("Received both homing confirmation messages")
+                                break
+                    except Exception as e:
+                        logger.error(f"Error reading homing response: {e}")
+
+                    time.sleep(0.1)
+
+                if not (state.homed_x and state.homed_y):
+                    logger.warning(f"Did not receive all homing messages (X:{state.homed_x}, Y:{state.homed_y})")
+
+                # Wait for idle state after $H
+                logger.info("Waiting for device to reach idle state after $H...")
+                idle_reached = check_idle()
+
+                if not idle_reached:
+                    logger.error("Device did not reach idle state after $H command")
+                    homing_complete.set()
+                    return
+
+                # Send x0 y0 to zero both positions using send_grbl_coordinates
+                logger.info("Zeroing positions with x0 y0")
+
+                # Run async function in new event loop
+                loop = asyncio.new_event_loop()
+                asyncio.set_event_loop(loop)
+                try:
+                    # Send x0 y0 command (G1 X0 Y0)
+                    result = loop.run_until_complete(send_grbl_coordinates(0, 0, 600))
+                    if result == False:
+                        logger.error("Position zeroing failed - send_grbl_coordinates returned False")
+                        homing_complete.set()
+                        return
+                finally:
+                    loop.close()
+
+                # Wait for idle state after zeroing
+                logger.info("Waiting for device to reach idle state after position zeroing...")
+                idle_reached = check_idle()
+
+                if not idle_reached:
+                    logger.error("Device did not reach idle state after position zeroing")
+                    homing_complete.set()
+                    return
+
+                # Set current position based on compass reference point (sensor mode only)
+                offset_radians = math.radians(state.angular_homing_offset_degrees)
+                state.current_theta = offset_radians
+                state.current_rho = 0
+
+                logger.info(f"Sensor homing completed - theta set to {state.angular_homing_offset_degrees}° ({offset_radians:.3f} rad), rho=0")
+
             else:
+                # Mode 0: Crash homing for radial axis (Y)
                 homing_speed = 400
                 if state.table_type == 'dune_weaver_mini':
                     homing_speed = 120
-                logger.info("Sensorless homing not supported. Using crash homing")
-                logger.info(f"Homing with speed {homing_speed}")
+
+                logger.info(f"Using crash homing mode at {homing_speed} mm/min")
 
                 # Run async function in new event loop
                 loop = asyncio.new_event_loop()
                 asyncio.set_event_loop(loop)
                 try:
                     if state.table_type == 'dune_weaver_mini':
-                        result = loop.run_until_complete(send_grbl_coordinates(0, - 30, homing_speed, home=True))
+                        result = loop.run_until_complete(send_grbl_coordinates(0, -30, homing_speed, home=True))
                         if result == False:
-                            logger.error("Homing failed - send_grbl_coordinates returned False")
+                            logger.error("Crash homing failed - send_grbl_coordinates returned False")
                             homing_complete.set()
                             return
                         state.machine_y -= 30
                     else:
                         result = loop.run_until_complete(send_grbl_coordinates(0, -22, homing_speed, home=True))
                         if result == False:
-                            logger.error("Homing failed - send_grbl_coordinates returned False")
+                            logger.error("Crash homing failed - send_grbl_coordinates returned False")
                             homing_complete.set()
                             return
                         state.machine_y -= 22
                 finally:
                     loop.close()
 
-            # Wait for device to reach idle state after homing
-            logger.info("Waiting for device to reach idle state after homing...")
-            idle_reached = check_idle()
-
-            if not idle_reached:
-                logger.error("Device did not reach idle state after homing")
-                homing_complete.set()
-                return
-            else:
-                state.current_theta = state.current_rho = 0
-
-            # Perform Desert Compass calibration if enabled (Raspberry Pi only)
-            if state.angular_homing_enabled:
-                logger.info("Starting Desert Compass calibration sequence")
-                try:
-                    # Initialize reed switch monitor with configured GPIO pin and invert state
-                    gpio_pin = state.angular_homing_gpio_pin
-                    invert_state = state.angular_homing_invert_state
-                    logger.info(f"Desert Compass: Using GPIO pin {gpio_pin} for reed switch (invert_state={invert_state})")
-                    reed_switch = ReedSwitchMonitor(gpio_pin=gpio_pin, invert_state=invert_state)
-
-                    try:
-                        # Reset theta first
-                        logger.info("Desert Compass: Resetting theta before calibration")
-                        asyncio.run(pattern_manager.reset_theta())
-
-                        # Move radial arm to perimeter (theta=0, rho=1.0)
-                        logger.info("Desert Compass: Moving radial arm to perimeter (theta=0, rho=1.0)")
-                        asyncio.run(pattern_manager.move_polar(0, 1.0, homing_speed))
-
-                        idle_reached = check_idle()
-
-                        if not idle_reached:
-                            logger.error("Desert Compass: Device did not reach idle state after moving to perimeter")
-                            homing_complete.set()
-                            return
-
-                        # Wait 1 second for stabilization
-                        logger.info("Desert Compass: Waiting for stabilization...")
-                        time.sleep(1)
-
-                        # Perform angular rotation until reed switch is triggered
-                        logger.info("Desert Compass: Rotating around perimeter to find reference point")
-                        increment = 0.1  # Small angular increment in radians
-                        current_theta = 0
-                        max_theta = 6.28  # One full rotation (2*pi)
-                        reed_switch_triggered = False
-
-                        while current_theta < max_theta:
-                            # Move to next position
-                            current_theta += increment
-                            asyncio.run(pattern_manager.move_polar(current_theta, 1.0, homing_speed))
-
-                            # Small delay to allow reed switch to settle after movement
-                            time.sleep(0.5)
-
-                            # Check reed switch AFTER movement completes
-                            if reed_switch.is_triggered():
-                                logger.info(f"Desert Compass: Reed switch triggered at theta={current_theta}")
-                                reed_switch_triggered = True
-                                break
-
-                        if not reed_switch_triggered:
-                            logger.warning("Desert Compass: Completed full rotation without reed switch trigger")
+                # Wait for device to reach idle state after crash homing
+                logger.info("Waiting for device to reach idle state after crash homing...")
+                idle_reached = check_idle()
 
-                        # Set theta to the offset value (accounting for sensor placement)
-                        # If offset is 0, this is the true reference position
-                        # If offset is non-zero, the sensor is physically placed at that angle
-                        # Convert degrees to radians for internal use
-                        import math
-                        offset_radians = math.radians(state.angular_homing_offset_degrees)
-                        state.current_theta = offset_radians
-                        state.current_rho = 1
-                        logger.info(f"Desert Compass: Calibration completed - theta set to {state.angular_homing_offset_degrees}° ({offset_radians:.3f} radians)")
+                if not idle_reached:
+                    logger.error("Device did not reach idle state after crash homing")
+                    homing_complete.set()
+                    return
 
-                    finally:
-                        reed_switch.cleanup()
+                # Crash homing just sets theta and rho to 0 (no x0 y0 command)
+                state.current_theta = 0
+                state.current_rho = 0
 
-                except Exception as e:
-                    logger.error(f"Error during Desert Compass calibration: {e}")
-                    # Continue with normal homing completion even if Desert Compass calibration fails
+                logger.info("Crash homing completed - theta=0, rho=0")
 
             homing_success = True
-            logger.info("Homing completed and device is idle")
-
             homing_complete.set()
+
         except Exception as e:
             logger.error(f"Error during homing: {e}")
             homing_complete.set()
-    
+
     # Start homing in a separate thread
     homing_thread = threading.Thread(target=home_internal)
     homing_thread.daemon = True
     homing_thread.start()
-    
+
     # Wait for homing to complete or timeout
     if not homing_complete.wait(timeout):
         logger.error(f"Homing timeout after {timeout} seconds")
@@ -677,11 +699,11 @@ def home(timeout=90):
         except Exception as e:
             logger.error(f"Error stopping movement after timeout: {e}")
         return False
-    
+
     if not homing_success:
         logger.error("Homing failed")
         return False
-    
+
     logger.info("Homing completed successfully")
     return True
 

+ 0 - 112
modules/connection/reed_switch.py

@@ -1,112 +0,0 @@
-"""
-Reed switch monitoring module for Raspberry Pi GPIO.
-Used for angular homing to detect home position.
-"""
-import logging
-import platform
-
-logger = logging.getLogger(__name__)
-
-class ReedSwitchMonitor:
-    """Monitor a reed switch connected to a Raspberry Pi GPIO pin."""
-
-    def __init__(self, gpio_pin=18, invert_state=False):
-        """
-        Initialize the reed switch monitor.
-
-        Args:
-            gpio_pin: GPIO pin number (BCM numbering) for the reed switch
-            invert_state: If True, invert the logic (triggered = LOW instead of HIGH)
-        """
-        self.gpio_pin = gpio_pin
-        self.invert_state = invert_state
-        self.gpio = None
-        self.is_raspberry_pi = False
-
-        # Try to import and initialize GPIO
-        try:
-            import RPi.GPIO as GPIO
-            self.gpio = GPIO
-
-            # Set up GPIO mode (BCM numbering)
-            self.gpio.setmode(GPIO.BCM)
-
-            # Set up the pin as input with pull-up resistor
-            # Reed switch should connect pin to ground when triggered
-            self.gpio.setup(self.gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
-            self.is_raspberry_pi = True
-            logger.info(f"Reed switch initialized on GPIO pin {self.gpio_pin} (invert_state={self.invert_state})")
-        except ImportError:
-            logger.warning("RPi.GPIO not available. Reed switch monitoring disabled.")
-        except Exception as e:
-            logger.error(f"Error initializing reed switch: {e}")
-            logger.info("Reed switch monitoring disabled.")
-
-    def is_triggered(self):
-        """
-        Check if the reed switch is currently triggered.
-
-        Returns:
-            bool: True if reed switch is triggered, False otherwise
-
-        Notes:
-            - If invert_state=False: triggered when pin is HIGH (1)
-            - If invert_state=True: triggered when pin is LOW (0)
-        """
-        if not self.is_raspberry_pi or not self.gpio:
-            return False
-
-        try:
-            # Read the GPIO pin state
-            pin_state = self.gpio.input(self.gpio_pin)
-
-            # Apply inversion if configured
-            if self.invert_state:
-                # Inverted: triggered when LOW (0)
-                return pin_state == 0
-            else:
-                # Normal: triggered when HIGH (1)
-                return pin_state == 1
-        except Exception as e:
-            logger.error(f"Error reading reed switch: {e}")
-            return False
-
-    def wait_for_trigger(self, timeout=None):
-        """
-        Wait for the reed switch to be triggered.
-
-        Args:
-            timeout: Maximum time to wait in seconds (None = wait indefinitely)
-
-        Returns:
-            bool: True if triggered, False if timeout occurred
-        """
-        if not self.is_raspberry_pi or not self.gpio:
-            logger.warning("Reed switch not available, cannot wait for trigger")
-            return False
-
-        try:
-            # Wait for rising edge (pin goes from LOW to HIGH)
-            channel = self.gpio.wait_for_edge(
-                self.gpio_pin,
-                self.gpio.RISING,
-                timeout=int(timeout * 1000) if timeout else None
-            )
-            return channel is not None
-        except Exception as e:
-            logger.error(f"Error waiting for reed switch trigger: {e}")
-            return False
-
-    def cleanup(self):
-        """Clean up GPIO resources."""
-        if self.is_raspberry_pi and self.gpio:
-            try:
-                self.gpio.cleanup(self.gpio_pin)
-                logger.info(f"Reed switch GPIO pin {self.gpio_pin} cleaned up")
-            except Exception as e:
-                logger.error(f"Error cleaning up reed switch GPIO: {e}")
-
-    def __del__(self):
-        """Destructor to ensure GPIO cleanup."""
-        self.cleanup()

+ 10 - 19
modules/core/state.py

@@ -32,20 +32,17 @@ class AppState:
         self.x_steps_per_mm = 0.0
         self.y_steps_per_mm = 0.0
         self.gear_ratio = 10
-        # 0 for crash homing, 1 for auto homing
+
+        # Homing mode: 0 = crash homing, 1 = sensor homing ($H)
         self.homing = 0
-        # Advanced calibration feature flag (Desert Compass)
-        # Set via environment variable ADVANCED_CALIBRATION=true
-        self.advanced_calibration_enabled = False
-
-        # Angular homing with reed switch (Raspberry Pi only)
-        self.angular_homing_enabled = False
-        # GPIO pin number (BCM numbering) for reed switch
-        self.angular_homing_gpio_pin = 18
-        # Invert the reed switch state (False = triggered on HIGH, True = triggered on LOW)
-        self.angular_homing_invert_state = False
-        # Angular offset in degrees for reed switch sensor placement
-        # This allows correcting for the physical position of the reed switch
+
+        # Homing state tracking (for sensor mode)
+        self.homed_x = False  # Set to True when [MSG:Homed:X] is received
+        self.homed_y = False  # Set to True when [MSG:Homed:Y] is received
+
+        # Angular homing compass reference point
+        # This is the angular offset in degrees where the sensor is placed
+        # After homing, theta will be set to this value
         self.angular_homing_offset_degrees = 0.0
 
         self.STATE_FILE = "state.json"
@@ -212,9 +209,6 @@ class AppState:
             "y_steps_per_mm": self.y_steps_per_mm,
             "gear_ratio": self.gear_ratio,
             "homing": self.homing,
-            "angular_homing_enabled": self.angular_homing_enabled,
-            "angular_homing_gpio_pin": self.angular_homing_gpio_pin,
-            "angular_homing_invert_state": self.angular_homing_invert_state,
             "angular_homing_offset_degrees": self.angular_homing_offset_degrees,
             "current_playlist": self._current_playlist,
             "current_playlist_name": self._current_playlist_name,
@@ -266,9 +260,6 @@ class AppState:
         self.y_steps_per_mm = data.get("y_steps_per_mm", 0.0)
         self.gear_ratio = data.get('gear_ratio', 10)
         self.homing = data.get('homing', 0)
-        self.angular_homing_enabled = data.get('angular_homing_enabled', False)
-        self.angular_homing_gpio_pin = data.get('angular_homing_gpio_pin', 18)
-        self.angular_homing_invert_state = data.get('angular_homing_invert_state', False)
         self.angular_homing_offset_degrees = data.get('angular_homing_offset_degrees', 0.0)
         self._current_playlist = data.get("current_playlist", None)
         self._current_playlist_name = data.get("current_playlist_name", None)

+ 72 - 85
static/js/settings.js

@@ -1152,7 +1152,7 @@ async function initializeauto_playMode() {
 document.addEventListener('DOMContentLoaded', function() {
     initializeauto_playMode();
     initializeStillSandsMode();
-    initializeAngularHomingConfig();
+    initializeHomingConfig();
 });
 
 // Still Sands Mode Functions
@@ -1492,115 +1492,110 @@ async function initializeStillSandsMode() {
     }
 }
 
-// Desert Compass Configuration Functions
-async function initializeAngularHomingConfig() {
-    logMessage('Checking Desert Compass feature availability', LOG_TYPE.INFO);
+// Homing Configuration
+async function initializeHomingConfig() {
+    logMessage('Initializing homing configuration', LOG_TYPE.INFO);
 
-    // Check if advanced calibration features are enabled
-    try {
-        const statusResponse = await fetch('/api/advanced-calibration-status');
-        const statusData = await statusResponse.json();
-
-        if (!statusData.enabled) {
-            logMessage('Advanced calibration features not enabled, skipping Desert Compass', LOG_TYPE.INFO);
-            return;
-        }
-
-        logMessage('Advanced calibration enabled, initializing Desert Compass', LOG_TYPE.INFO);
-
-        // Show the Desert Compass section
-        const desertCompassSection = document.getElementById('desertCompassSection');
-        if (desertCompassSection) {
-            desertCompassSection.style.display = 'block';
-        }
-    } catch (error) {
-        logMessage(`Failed to check advanced calibration status: ${error.message}`, LOG_TYPE.ERROR);
-        return;
-    }
-
-    const angularHomingToggle = document.getElementById('angularHomingToggle');
-    const angularHomingInfo = document.getElementById('angularHomingInfo');
-    const gpioSelectionContainer = document.getElementById('gpioSelectionContainer');
-    const gpioInput = document.getElementById('gpioInput');
-    const invertStateContainer = document.getElementById('invertStateContainer');
-    const invertStateToggle = document.getElementById('invertStateToggle');
-    const angularOffsetContainer = document.getElementById('angularOffsetContainer');
+    const homingModeCrash = document.getElementById('homingModeCrash');
+    const homingModeSensor = document.getElementById('homingModeSensor');
     const angularOffsetInput = document.getElementById('angularOffsetInput');
+    const compassOffsetContainer = document.getElementById('compassOffsetContainer');
     const saveHomingConfigButton = document.getElementById('saveHomingConfig');
+    const homingInfoContent = document.getElementById('homingInfoContent');
 
     // Check if elements exist
-    if (!angularHomingToggle || !angularHomingInfo || !saveHomingConfigButton ||
-        !gpioSelectionContainer || !gpioInput || !invertStateContainer ||
-        !invertStateToggle || !angularOffsetContainer || !angularOffsetInput) {
-        logMessage('Desert Compass elements not found, skipping initialization', LOG_TYPE.WARNING);
+    if (!homingModeCrash || !homingModeSensor || !angularOffsetInput || !saveHomingConfigButton || !homingInfoContent || !compassOffsetContainer) {
+        logMessage('Homing configuration elements not found, skipping initialization', LOG_TYPE.WARNING);
         return;
     }
 
-    logMessage('All Desert Compass elements found successfully', LOG_TYPE.INFO);
+    logMessage('Homing configuration elements found successfully', LOG_TYPE.INFO);
 
-    // Load current Desert Compass settings
+    // Function to get selected homing mode
+    function getSelectedMode() {
+        return homingModeCrash.checked ? 0 : 1;
+    }
+
+    // Function to update info box and visibility based on selected mode
+    function updateHomingInfo() {
+        const mode = getSelectedMode();
+
+        // Show/hide compass offset based on mode
+        if (mode === 0) {
+            compassOffsetContainer.style.display = 'none';
+            homingInfoContent.innerHTML = `
+                <p class="font-medium text-blue-800">Crash Homing Mode:</p>
+                <ul class="mt-1 space-y-1 text-blue-700">
+                    <li>• Y axis moves -22mm (or -30mm for mini) until physical stop</li>
+                    <li>• Theta set to 0, rho set to 0</li>
+                    <li>• No x0 y0 command sent</li>
+                    <li>• No hardware sensors required</li>
+                </ul>
+            `;
+        } else {
+            compassOffsetContainer.style.display = 'block';
+            homingInfoContent.innerHTML = `
+                <p class="font-medium text-blue-800">Sensor Homing Mode ($H):</p>
+                <ul class="mt-1 space-y-1 text-blue-700">
+                    <li>• $H command homes both X and Y axes</li>
+                    <li>• Waits for [MSG:Homed:X] and [MSG:Homed:Y] confirmation</li>
+                    <li>• Sends x0 y0 to zero positions</li>
+                    <li>• Theta set to compass reference point, rho set to 0</li>
+                    <li>• Requires hardware limit switches configured in FluidNC firmware</li>
+                </ul>
+            `;
+        }
+    }
+
+    // Load current homing configuration
     try {
-        const response = await fetch('/api/angular-homing');
+        const response = await fetch('/api/homing-config');
         const data = await response.json();
 
-        angularHomingToggle.checked = data.angular_homing_enabled || false;
-        gpioInput.value = data.angular_homing_gpio_pin || 18;
-        invertStateToggle.checked = data.angular_homing_invert_state || false;
+        // Set radio button based on mode
+        if (data.homing_mode === 1) {
+            homingModeSensor.checked = true;
+        } else {
+            homingModeCrash.checked = true;
+        }
+
         angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
+        updateHomingInfo();
 
-        if (data.angular_homing_enabled) {
-            angularHomingInfo.style.display = 'block';
-            gpioSelectionContainer.style.display = 'block';
-            invertStateContainer.style.display = 'block';
-            angularOffsetContainer.style.display = 'block';
-        }
+        logMessage(`Loaded homing config: mode=${data.homing_mode}, offset=${data.angular_homing_offset_degrees}°`, LOG_TYPE.INFO);
     } catch (error) {
-        logMessage(`Error loading Desert Compass settings: ${error.message}`, LOG_TYPE.ERROR);
+        logMessage(`Error loading homing configuration: ${error.message}`, LOG_TYPE.ERROR);
         // Initialize with defaults if load fails
-        angularHomingToggle.checked = false;
-        gpioInput.value = 18;
-        invertStateToggle.checked = false;
+        homingModeCrash.checked = true;
         angularOffsetInput.value = 0;
-        angularHomingInfo.style.display = 'none';
-        gpioSelectionContainer.style.display = 'none';
-        invertStateContainer.style.display = 'none';
-        angularOffsetContainer.style.display = 'none';
+        updateHomingInfo();
     }
 
-    // Function to save Desert Compass settings
-    async function saveAngularHomingSettings() {
-        // Validate GPIO pin
-        const gpioPin = parseInt(gpioInput.value);
-        if (isNaN(gpioPin) || gpioPin < 2 || gpioPin > 27) {
-            showStatusMessage('GPIO pin must be between 2 and 27', 'error');
-            return;
-        }
-
+    // Function to save homing configuration
+    async function saveHomingConfig() {
         // Update button UI to show loading state
         const originalButtonHTML = saveHomingConfigButton.innerHTML;
         saveHomingConfigButton.disabled = true;
         saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
 
         try {
-            const response = await fetch('/api/angular-homing', {
+            const response = await fetch('/api/homing-config', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({
-                    angular_homing_enabled: angularHomingToggle.checked,
-                    angular_homing_gpio_pin: gpioPin,
-                    angular_homing_invert_state: invertStateToggle.checked,
+                    homing_mode: getSelectedMode(),
                     angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
                 })
             });
 
             if (!response.ok) {
                 const errorData = await response.json();
-                throw new Error(errorData.detail || 'Failed to save Desert Compass settings');
+                throw new Error(errorData.detail || 'Failed to save homing configuration');
             }
 
             // Show success state temporarily
             saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
-            showStatusMessage('Desert Compass configuration saved successfully', 'success');
+            showStatusMessage('Homing configuration saved successfully', 'success');
 
             // Restore button after 2 seconds
             setTimeout(() => {
@@ -1608,8 +1603,8 @@ async function initializeAngularHomingConfig() {
                 saveHomingConfigButton.disabled = false;
             }, 2000);
         } catch (error) {
-            logMessage(`Error saving Desert Compass settings: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
+            logMessage(`Error saving homing configuration: ${error.message}`, LOG_TYPE.ERROR);
+            showStatusMessage(`Failed to save homing configuration: ${error.message}`, 'error');
 
             // Restore button immediately on error
             saveHomingConfigButton.innerHTML = originalButtonHTML;
@@ -1618,15 +1613,7 @@ async function initializeAngularHomingConfig() {
     }
 
     // Event listeners
-    angularHomingToggle.addEventListener('change', () => {
-        logMessage(`Desert Compass toggle changed: ${angularHomingToggle.checked}`, LOG_TYPE.INFO);
-        const isEnabled = angularHomingToggle.checked;
-        angularHomingInfo.style.display = isEnabled ? 'block' : 'none';
-        gpioSelectionContainer.style.display = isEnabled ? 'block' : 'none';
-        invertStateContainer.style.display = isEnabled ? 'block' : 'none';
-        angularOffsetContainer.style.display = isEnabled ? 'block' : 'none';
-        logMessage(`Info display set to: ${angularHomingInfo.style.display}`, LOG_TYPE.INFO);
-    });
-
-    saveHomingConfigButton.addEventListener('click', saveAngularHomingSettings);
+    homingModeCrash.addEventListener('change', updateHomingInfo);
+    homingModeSensor.addEventListener('change', updateHomingInfo);
+    saveHomingConfigButton.addEventListener('click', saveHomingConfig);
 }

+ 54 - 68
templates/settings.html

@@ -328,75 +328,63 @@ input:checked + .slider:before {
       </div>
     </div>
   </section>
-  <!-- Desert Compass Section (hidden unless advanced calibration enabled) -->
-  <section id="desertCompassSection" class="bg-white rounded-xl shadow-sm overflow-hidden" style="display: none;">
+  <!-- Homing Configuration Section -->
+  <section id="homingSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
     >
-      Desert Compass
+      Homing Configuration
     </h2>
     <div class="px-6 py-5 space-y-6">
-      <div class="flex items-center justify-between">
-        <div class="flex-1">
-          <h3 class="text-slate-700 text-base font-medium leading-normal flex items-center gap-2">
-            <span class="material-icons text-slate-600">explore</span>
-            Enable Desert Compass (Raspberry Pi Only)
-          </h3>
-          <p class="text-xs text-slate-500 mt-1">
-            Use a reed switch and magnet to establish a precise angular reference point for the table's rotation.
-          </p>
-        </div>
-        <label class="switch">
-          <input type="checkbox" id="angularHomingToggle">
-          <span class="slider round"></span>
+      <!-- Homing Mode Selection -->
+      <div class="space-y-3">
+        <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
+          <span class="material-icons text-slate-600 text-base">home</span>
+          Homing Mode
         </label>
-      </div>
 
-      <!-- GPIO Pin Selection (shown when Desert Compass is enabled) -->
-      <div id="gpioSelectionContainer" class="space-y-2" style="display: none;">
-        <label for="gpioInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
-          <span class="material-icons text-slate-600 text-base">settings_input_component</span>
-          Reed Switch GPIO Pin
-        </label>
-        <input
-          type="number"
-          id="gpioInput"
-          min="2"
-          max="27"
-          step="1"
-          value="18"
-          class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 text-sm"
-          placeholder="18"
-        />
-        <p class="text-xs text-slate-500">
-          GPIO pin number (BCM numbering) where the reed switch is connected. Common pins: 17, 18, 22, 23, 24, 25, 27.
-        </p>
-      </div>
+        <div class="space-y-3">
+          <!-- Crash Homing Option -->
+          <label class="flex items-start gap-3 p-3 border border-slate-300 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors">
+            <input
+              type="radio"
+              name="homingMode"
+              value="0"
+              id="homingModeCrash"
+              class="mt-0.5 w-4 h-4 text-sky-600 focus:ring-sky-500"
+            />
+            <div class="flex-1">
+              <div class="text-sm font-medium text-slate-700">Crash Homing</div>
+              <div class="text-xs text-slate-500 mt-1">
+                Y axis moves until physical stop, then theta and rho set to 0 (no x0 y0 command)
+              </div>
+            </div>
+          </label>
 
-      <!-- Invert State Toggle (shown when Desert Compass is enabled) -->
-      <div id="invertStateContainer" class="space-y-2" style="display: none;">
-        <div class="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
-          <div class="flex-1">
-            <label for="invertStateToggle" class="text-sm font-medium text-slate-700 flex items-center gap-2 cursor-pointer">
-              <span class="material-icons text-slate-600 text-base">swap_vert</span>
-              Invert Sensor Logic
-            </label>
-            <p class="text-xs text-slate-500 mt-1">
-              Enable if your reed switch is triggered when LOW instead of HIGH (normally closed configuration).
-            </p>
-          </div>
-          <label class="switch">
-            <input type="checkbox" id="invertStateToggle">
-            <span class="slider round"></span>
+          <!-- Sensor Homing Option -->
+          <label class="flex items-start gap-3 p-3 border border-slate-300 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors">
+            <input
+              type="radio"
+              name="homingMode"
+              value="1"
+              id="homingModeSensor"
+              class="mt-0.5 w-4 h-4 text-sky-600 focus:ring-sky-500"
+            />
+            <div class="flex-1">
+              <div class="text-sm font-medium text-slate-700">Sensor Homing ($H command)</div>
+              <div class="text-xs text-slate-500 mt-1">
+                Homes both X and Y axes via FluidNC firmware, then zeroes positions with x0 y0
+              </div>
+            </div>
           </label>
         </div>
       </div>
 
-      <!-- Compass Calibration (shown when Desert Compass is enabled) -->
-      <div id="angularOffsetContainer" class="space-y-2" style="display: none;">
+      <!-- Compass Reference Point (Sensor mode only) -->
+      <div id="compassOffsetContainer" class="space-y-2">
         <label for="angularOffsetInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
-          <span class="material-icons text-slate-600 text-base">straighten</span>
-          Compass Reference Point (degrees)
+          <span class="material-icons text-slate-600 text-base">explore</span>
+          Compass Reference Point (degrees) <span class="text-xs text-slate-400">(Sensor mode only)</span>
         </label>
         <input
           type="number"
@@ -409,23 +397,21 @@ input:checked + .slider:before {
           placeholder="0.0"
         />
         <p class="text-xs text-slate-500">
-          Set the angle (in degrees) where your magnet/sensor is physically mounted. 0° = East, increases clockwise (90° = South, 180° = West, 270° = North).
+          Set the angle (in degrees) where your angular sensor is physically mounted. After sensor homing, theta will be set to this value. 0° = East, increases clockwise (90° = South, 180° = West, 270° = North).
         </p>
       </div>
 
-      <div id="angularHomingInfo" class="text-xs text-slate-600 bg-blue-50 border border-blue-200 rounded-lg p-3" style="display: none;">
+      <!-- Homing Info Box -->
+      <div id="homingInfoBox" class="text-xs text-slate-600 bg-blue-50 border border-blue-200 rounded-lg p-3">
         <div class="flex items-start gap-2">
           <span class="material-icons text-blue-600 text-base">info</span>
-          <div>
-            <p class="font-medium text-blue-800">How Desert Compass Works:</p>
+          <div id="homingInfoContent">
+            <p class="font-medium text-blue-800">Crash Homing Mode:</p>
             <ul class="mt-1 space-y-1 text-blue-700">
-              <li>• After radial calibration, the arm moves to the table's edge</li>
-              <li>• The table rotates until the magnet triggers the reed switch</li>
-              <li>• This position becomes the angular reference point based on your compass setting</li>
-              <li>• Requires Raspberry Pi with reed switch connected to selected GPIO pin</li>
-              <li>• Standard wiring: Reed switch to 3.3V (triggered = HIGH)</li>
-              <li>• Inverted wiring: Reed switch to ground (triggered = LOW)</li>
-              <li>• Uses BCM GPIO numbering (not physical pin numbers)</li>
+              <li>• Y axis moves -22mm (or -30mm for mini) until physical stop</li>
+              <li>• Theta set to 0, rho set to 0</li>
+              <li>• No x0 y0 command sent</li>
+              <li>• No hardware sensors required</li>
             </ul>
           </div>
         </div>
@@ -437,7 +423,7 @@ input:checked + .slider:before {
           class="flex items-center justify-center gap-2 min-w-[140px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors"
         >
           <span class="material-icons text-lg">save</span>
-          <span class="truncate">Save Compass Config</span>
+          <span class="truncate">Save Configuration</span>
         </button>
       </div>
     </div>

+ 0 - 155
test_reed_switch.py

@@ -1,155 +0,0 @@
-#!/usr/bin/env python3
-"""
-Simple test script to verify reed switch functionality.
-Run this script on your Raspberry Pi to test the reed switch.
-
-Usage:
-    python test_reed_switch.py [--gpio PIN] [--invert]
-
-Arguments:
-    --gpio PIN    GPIO pin number (BCM numbering) to test (default: 18)
-    --invert      Invert the switch logic (triggered = LOW instead of HIGH)
-
-Examples:
-    python test_reed_switch.py                    # Test GPIO 18 (default, normal logic)
-    python test_reed_switch.py --gpio 17          # Test GPIO 17 (normal logic)
-    python test_reed_switch.py --gpio 22 --invert # Test GPIO 22 (inverted logic)
-"""
-
-import time
-import sys
-import argparse
-
-try:
-    from modules.connection.reed_switch import ReedSwitchMonitor
-except ImportError:
-    print("Error: Could not import ReedSwitchMonitor")
-    print("Make sure you're running this from the dune-weaver directory")
-    sys.exit(1)
-
-def main(gpio_pin=18, invert_state=False):
-    """
-    Test the reed switch on the specified GPIO pin.
-
-    Args:
-        gpio_pin: GPIO pin number (BCM numbering) to test
-        invert_state: If True, invert the switch logic (triggered = LOW)
-    """
-    print("=" * 60)
-    print(f"Reed Switch Test - GPIO {gpio_pin}")
-    if invert_state:
-        print("(Inverted Logic: Triggered = LOW)")
-    else:
-        print("(Normal Logic: Triggered = HIGH)")
-    print("=" * 60)
-    print()
-
-    # Initialize the reed switch monitor
-    print(f"Initializing reed switch monitor on GPIO {gpio_pin}...")
-    if invert_state:
-        print("Using inverted logic (triggered when pin is LOW)")
-    else:
-        print("Using normal logic (triggered when pin is HIGH)")
-    reed_switch = ReedSwitchMonitor(gpio_pin=gpio_pin, invert_state=invert_state)
-
-    # Check if we're on a Raspberry Pi
-    if not reed_switch.is_raspberry_pi:
-        print("❌ ERROR: Not running on a Raspberry Pi!")
-        print("This test must be run on a Raspberry Pi with GPIO support.")
-        return
-
-    print("✓ Running on Raspberry Pi")
-    print("✓ GPIO initialized successfully")
-    print()
-    print("=" * 60)
-    print("MONITORING REED SWITCH")
-    print("=" * 60)
-    print()
-    print("Instructions:")
-    print("  • The reed switch should be connected:")
-    print(f"    - One terminal → GPIO {gpio_pin}")
-    if invert_state:
-        print("    - Other terminal → Ground (for inverted logic)")
-        print("    - Pull-up resistor enabled internally")
-    else:
-        print("    - Other terminal → 3.3V (for normal logic)")
-        print("    - Or use internal pull-up and connect to ground")
-    print()
-    print("  • Bring a magnet close to the reed switch to trigger it")
-    print("  • You should see 'TRIGGERED!' when the switch closes")
-    print("  • Press Ctrl+C to exit")
-    print()
-    print("-" * 60)
-
-    try:
-        last_state = None
-        trigger_count = 0
-
-        while True:
-            # Check if reed switch is triggered
-            is_triggered = reed_switch.is_triggered()
-
-            # Only print when state changes (to avoid spam)
-            if is_triggered != last_state:
-                if is_triggered:
-                    trigger_count += 1
-                    print(f"🔴 TRIGGERED! (count: {trigger_count})")
-                else:
-                    print("⚪ Not triggered")
-
-                last_state = is_triggered
-
-            # Small delay to avoid overwhelming the GPIO
-            time.sleep(0.05)
-
-    except KeyboardInterrupt:
-        print()
-        print("-" * 60)
-        print(f"✓ Test completed. Reed switch was triggered {trigger_count} times.")
-        print()
-
-    finally:
-        # Clean up GPIO
-        reed_switch.cleanup()
-        print("✓ GPIO cleaned up")
-        print()
-
-if __name__ == "__main__":
-    # Parse command-line arguments
-    parser = argparse.ArgumentParser(
-        description="Test reed switch functionality on Raspberry Pi GPIO pins",
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog="""
-Examples:
-  python test_reed_switch.py                    # Test GPIO 18 (normal logic)
-  python test_reed_switch.py --gpio 17          # Test GPIO 17 (normal logic)
-  python test_reed_switch.py --gpio 22 --invert # Test GPIO 22 (inverted logic)
-
-Note: Uses BCM GPIO numbering (not physical pin numbers)
-      Normal logic: Triggered when HIGH (connected to 3.3V)
-      Inverted logic: Triggered when LOW (connected to ground)
-        """
-    )
-    parser.add_argument(
-        '--gpio',
-        type=int,
-        default=18,
-        metavar='PIN',
-        help='GPIO pin number to test (BCM numbering, default: 18)'
-    )
-    parser.add_argument(
-        '--invert',
-        action='store_true',
-        help='Invert the switch logic (triggered = LOW instead of HIGH)'
-    )
-
-    args = parser.parse_args()
-
-    # Validate GPIO pin range
-    if args.gpio < 2 or args.gpio > 27:
-        print(f"❌ ERROR: GPIO pin must be between 2 and 27 (got {args.gpio})")
-        print("Valid GPIO pins: 2-27 (BCM numbering)")
-        sys.exit(1)
-
-    # Run the test
-    main(gpio_pin=args.gpio, invert_state=args.invert)