Prechádzať zdrojové kódy

feat: add sensor homing failure recovery popup

When sensor homing fails during startup or manual homing:
- Show a blocking popup notifying user to check sensor position
- Provide "Retry Sensor Homing" option
- Provide "Switch to Crash Homing" option
- Connection is not established until user takes action
- Add /recover_sensor_homing API endpoint for recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 týždeň pred
rodič
commit
12ca98147e

+ 111 - 4
frontend/src/components/layout/Layout.tsx

@@ -66,6 +66,10 @@ export function Layout() {
   const [connectionAttempts, setConnectionAttempts] = useState(0)
   const wsRef = useRef<WebSocket | null>(null)
 
+  // Sensor homing failure state
+  const [sensorHomingFailed, setSensorHomingFailed] = useState(false)
+  const [isRecoveringHoming, setIsRecoveringHoming] = useState(false)
+
   // Fetch app settings
   const fetchAppSettings = () => {
     apiClient.get<{ app?: { name?: string; custom_logo?: string } }>('/api/settings')
@@ -253,13 +257,20 @@ export function Layout() {
               // Detect transition from homing to not homing
               if (wasHomingRef.current && !newIsHoming) {
                 // Homing just completed - show completion state with countdown
-                setHomingJustCompleted(true)
-                setHomingCountdown(5)
-                setHomingDismissed(false)
+                // But not if sensor homing failed (that shows a different dialog)
+                if (!data.data.sensor_homing_failed) {
+                  setHomingJustCompleted(true)
+                  setHomingCountdown(5)
+                  setHomingDismissed(false)
+                }
               }
               wasHomingRef.current = newIsHoming
               setIsHoming(newIsHoming)
             }
+            // Update sensor homing failure status
+            if (data.data.sensor_homing_failed !== undefined) {
+              setSensorHomingFailed(data.data.sensor_homing_failed)
+            }
             // Auto-open/close Now Playing bar based on playback state
             // Track current file - this is the most reliable indicator of playback
             const currentFile = data.data.current_file || null
@@ -319,6 +330,7 @@ export function Layout() {
         setCurrentPlayingFile(null) // Reset playback state for new table
         setIsConnected(false) // Reset connection status until new table reports
         setIsBackendConnected(false) // Show connecting state
+        setSensorHomingFailed(false) // Reset sensor homing failure state for new table
         connectWebSocket()
       }
     })
@@ -622,6 +634,34 @@ export function Layout() {
     }
   }
 
+  // Handle sensor homing recovery
+  const handleSensorHomingRecovery = async (switchToCrashHoming: boolean) => {
+    setIsRecoveringHoming(true)
+    try {
+      const response = await apiClient.post<{
+        success: boolean
+        sensor_homing_failed?: boolean
+        message?: string
+      }>('/recover_sensor_homing', {
+        switch_to_crash_homing: switchToCrashHoming
+      })
+
+      if (response.success) {
+        toast.success(response.message || 'Homing completed successfully')
+        setSensorHomingFailed(false)
+      } else if (response.sensor_homing_failed) {
+        // Sensor homing failed again
+        toast.error(response.message || 'Sensor homing failed again')
+      } else {
+        toast.error(response.message || 'Recovery failed')
+      }
+    } catch {
+      toast.error('Failed to recover from sensor homing failure')
+    } finally {
+      setIsRecoveringHoming(false)
+    }
+  }
+
   // Update document title based on current page
   useEffect(() => {
     const currentNav = navItems.find((item) => item.path === location.pathname)
@@ -950,6 +990,72 @@ export function Layout() {
 
   return (
     <div className="min-h-dvh bg-background flex flex-col">
+      {/* Sensor Homing Failure Popup */}
+      {sensorHomingFailed && (
+        <div className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
+          <div className="bg-background rounded-lg shadow-xl w-full max-w-md border border-destructive/30">
+            <div className="p-6">
+              <div className="text-center space-y-4">
+                <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-destructive/10 mb-2">
+                  <span className="material-icons-outlined text-4xl text-destructive">
+                    error_outline
+                  </span>
+                </div>
+                <h2 className="text-xl font-semibold">Sensor Homing Failed</h2>
+                <p className="text-muted-foreground text-sm">
+                  The sensor homing process could not complete. The limit sensors may not be positioned correctly or may be malfunctioning.
+                </p>
+
+                <div className="bg-amber-500/10 border border-amber-500/20 p-3 rounded-lg text-sm text-left">
+                  <p className="text-amber-600 dark:text-amber-400 font-medium mb-2">
+                    Troubleshooting steps:
+                  </p>
+                  <ul className="text-amber-600 dark:text-amber-400 space-y-1 list-disc list-inside">
+                    <li>Check that the limit sensors are properly connected</li>
+                    <li>Verify the sensor positions are correct</li>
+                    <li>Ensure nothing is blocking the sensor path</li>
+                    <li>Check for loose wiring connections</li>
+                  </ul>
+                </div>
+
+                <p className="text-muted-foreground text-sm">
+                  Connection will not be established until this is resolved.
+                </p>
+
+                {/* Action Buttons */}
+                {!isRecoveringHoming ? (
+                  <div className="flex flex-col gap-2 pt-2">
+                    <Button
+                      onClick={() => handleSensorHomingRecovery(false)}
+                      className="w-full gap-2"
+                    >
+                      <span className="material-icons text-base">refresh</span>
+                      Retry Sensor Homing
+                    </Button>
+                    <Button
+                      variant="secondary"
+                      onClick={() => handleSensorHomingRecovery(true)}
+                      className="w-full gap-2"
+                    >
+                      <span className="material-icons text-base">sync_alt</span>
+                      Switch to Crash Homing
+                    </Button>
+                    <p className="text-xs text-muted-foreground">
+                      Crash homing moves the arm to a physical stop without using sensors.
+                    </p>
+                  </div>
+                ) : (
+                  <div className="flex items-center justify-center gap-2 py-4">
+                    <span className="material-icons-outlined text-primary animate-spin">sync</span>
+                    <span className="text-muted-foreground">Attempting recovery...</span>
+                  </div>
+                )}
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+
       {/* Cache Progress Blocking Overlay */}
       {cacheProgress?.is_running && (
         <div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col items-center justify-center p-4">
@@ -1072,7 +1178,8 @@ export function Layout() {
       )}
 
       {/* Backend Connection / Homing Blocking Overlay */}
-      {(!isBackendConnected || (isHoming && !homingDismissed) || homingJustCompleted) && (
+      {/* Don't show this overlay when sensor homing failed - that has its own dialog */}
+      {!sensorHomingFailed && (!isBackendConnected || (isHoming && !homingDismissed) || homingJustCompleted) && (
         <div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col items-center justify-center p-4">
           <div className="w-full max-w-2xl space-y-6">
             {/* Status Header */}

+ 1 - 0
frontend/vite.config.ts

@@ -143,6 +143,7 @@ export default defineConfig({
       '/list_serial_ports': 'http://localhost:8080',
       '/connect': 'http://localhost:8080',
       '/disconnect': 'http://localhost:8080',
+      '/recover_sensor_homing': 'http://localhost:8080',
       // Patterns
       '/list_theta_rho_files': 'http://localhost:8080',
       '/list_theta_rho_files_with_metadata': 'http://localhost:8080',

+ 78 - 0
main.py

@@ -117,6 +117,13 @@ async def lifespan(app: FastAPI):
                     success = await asyncio.to_thread(connection_manager.home)
                     if not success:
                         logger.warning("Background homing failed or was skipped")
+                        # If sensor homing failed, close connection and wait for user action
+                        if state.sensor_homing_failed:
+                            logger.error("Sensor homing failed - closing connection. User must check sensor or switch to crash homing.")
+                            if state.conn:
+                                await asyncio.to_thread(state.conn.close)
+                                state.conn = None
+                            return  # Don't proceed with auto-play
                 finally:
                     state.is_homing = False
                     logger.info("Background homing completed")
@@ -1870,6 +1877,77 @@ async def send_home():
         logger.error(f"Failed to send home command: {str(e)}")
         raise HTTPException(status_code=500, detail=str(e))
 
+class SensorHomingRecoveryRequest(BaseModel):
+    switch_to_crash_homing: bool = False
+
+@app.post("/recover_sensor_homing")
+async def recover_sensor_homing(request: SensorHomingRecoveryRequest):
+    """
+    Recover from sensor homing failure.
+
+    If switch_to_crash_homing is True, changes homing mode to crash homing (mode 0)
+    and saves the setting. Then attempts to reconnect and home the device.
+
+    If switch_to_crash_homing is False, just clears the failure flag and retries
+    with sensor homing.
+    """
+    try:
+        # Clear the sensor homing failure flag first
+        state.sensor_homing_failed = False
+
+        if request.switch_to_crash_homing:
+            # Switch to crash homing mode
+            logger.info("Switching to crash homing mode per user request")
+            state.homing = 0
+            state.homing_user_override = True
+            state.save()
+
+        # If already connected, just perform homing
+        if state.conn and state.conn.is_connected():
+            logger.info("Device already connected, performing homing...")
+            state.is_homing = True
+            try:
+                success = await asyncio.to_thread(connection_manager.home)
+                if not success:
+                    # Check if sensor homing failed again
+                    if state.sensor_homing_failed:
+                        return {
+                            "success": False,
+                            "sensor_homing_failed": True,
+                            "message": "Sensor homing failed again. Please check sensor position or switch to crash homing."
+                        }
+                    return {"success": False, "message": "Homing failed"}
+                return {"success": True, "message": "Homing completed successfully"}
+            finally:
+                state.is_homing = False
+        else:
+            # Need to reconnect
+            logger.info("Reconnecting device and performing homing...")
+            state.is_homing = True
+            try:
+                # connect_device includes homing
+                await asyncio.to_thread(connection_manager.connect_device, True)
+
+                # Check if sensor homing failed during connection
+                if state.sensor_homing_failed:
+                    return {
+                        "success": False,
+                        "sensor_homing_failed": True,
+                        "message": "Sensor homing failed. Please check sensor position or switch to crash homing."
+                    }
+
+                if state.conn and state.conn.is_connected():
+                    return {"success": True, "message": "Connected and homed successfully"}
+                else:
+                    return {"success": False, "message": "Failed to establish connection"}
+            finally:
+                state.is_homing = False
+
+    except Exception as e:
+        logger.error(f"Error during sensor homing recovery: {e}")
+        state.is_homing = False
+        raise HTTPException(status_code=500, detail=str(e))
+
 @app.post("/run_theta_rho_file/{file_name}")
 async def run_specific_theta_rho_file(file_name: str):
     file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)

+ 12 - 1
modules/connection/connection_manager.py

@@ -241,6 +241,13 @@ def device_init(homing=True):
         success = home()
         if not success:
             logger.error("Homing failed during device initialization")
+            # If sensor homing failed, close connection and return False
+            # This prevents auto-connection from completing until user takes action
+            if state.sensor_homing_failed:
+                logger.error("Sensor homing failed - closing connection. User must check sensor or switch to crash homing.")
+                state.conn.close()
+                state.conn = None
+                return False
 
     time.sleep(2)  # Allow time for the connection to establish
     return True
@@ -1083,7 +1090,9 @@ def home(timeout=120):
 
                 elif not state.homed_x and not state.homed_y:
                     # Neither axis homed - this is a failure, don't proceed
-                    logger.error("Sensor homing failed - neither axis homed")
+                    # Set sensor_homing_failed flag to notify UI for user action
+                    logger.error("Sensor homing failed - neither axis homed. User action required.")
+                    state.sensor_homing_failed = True
                     homing_complete.set()
                     return
                 else:
@@ -1173,6 +1182,8 @@ def home(timeout=120):
                 logger.error(f"Error updating machine position after homing: {e}")
 
             homing_success = True
+            # Clear sensor_homing_failed flag on successful homing
+            state.sensor_homing_failed = False
             homing_complete.set()
 
         except Exception as e:

+ 1 - 0
modules/core/pattern_manager.py

@@ -1765,6 +1765,7 @@ def get_status():
         "scheduled_pause": is_in_scheduled_pause_period(),
         "is_running": bool(state.current_playing_file and not state.stop_requested),
         "is_homing": state.is_homing,
+        "sensor_homing_failed": state.sensor_homing_failed,
         "is_clearing": state.is_clearing,
         "progress": None,
         "playlist": None,

+ 4 - 0
modules/core/state.py

@@ -63,6 +63,10 @@ class AppState:
         # Homing in progress flag - blocks other movement operations
         self.is_homing = False
 
+        # Sensor homing failure flag - set when sensor homing fails
+        # This indicates to the UI that sensor homing failed and user action is needed
+        self.sensor_homing_failed = False
+
         # 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