Bladeren bron

Fix serial stability issues caused by asyncio event overhead

- Make stop_requested/skip_requested setters thread-safe using
  call_soon_threadsafe() for cross-thread event manipulation
- Remove state.stop_requested=False from move_polar() which was
  triggering event manipulation thousands of times per pattern
- Add stop_requested clearing to manual move endpoints instead
  (/move_to_center, /move_to_perimeter, /send_coordinate)
- Fix BrowsePage upload refresh to call fetchPatterns()

The root cause was move_polar() setting stop_requested on every
coordinate, causing constant asyncio event manipulation that
created race conditions and overhead during pattern execution.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 week geleden
bovenliggende
commit
5b7804685b
4 gewijzigde bestanden met toevoegingen van 59 en 16 verwijderingen
  1. 2 3
      frontend/src/pages/BrowsePage.tsx
  2. 9 0
      main.py
  3. 2 3
      modules/core/pattern_manager.py
  4. 46 10
      modules/core/state.py

+ 2 - 3
frontend/src/pages/BrowsePage.tsx

@@ -819,9 +819,8 @@ export function BrowsePage() {
       await apiClient.uploadFile('/upload_theta_rho', file)
       toast.success(`Pattern "${file.name}" uploaded successfully`)
 
-      // Refresh patterns list
-      const data = await apiClient.get<{ files?: PatternMetadata[] }>('/list_theta_rho_files')
-      setPatterns(data.files || [])
+      // Refresh patterns list using the same function as initial load
+      await fetchPatterns()
     } catch (error) {
       console.error('Upload error:', error)
       toast.error(error instanceof Error ? error.message : 'Failed to upload pattern')

+ 9 - 0
main.py

@@ -1950,6 +1950,9 @@ async def move_to_center():
 
         check_homing_in_progress()
 
+        # Clear stop_requested to ensure manual move works after pattern stop
+        state.stop_requested = False
+
         logger.info("Moving device to center position")
         await pattern_manager.reset_theta()
         await pattern_manager.move_polar(0, 0)
@@ -1969,6 +1972,9 @@ async def move_to_perimeter():
 
         check_homing_in_progress()
 
+        # Clear stop_requested to ensure manual move works after pattern stop
+        state.stop_requested = False
+
         await pattern_manager.reset_theta()
         await pattern_manager.move_polar(0, 1)
         return {"success": True}
@@ -2144,6 +2150,9 @@ async def send_coordinate(request: CoordinateRequest):
 
     check_homing_in_progress()
 
+    # Clear stop_requested to ensure manual move works after pattern stop
+    state.stop_requested = False
+
     try:
         logger.debug(f"Sending coordinate: theta={request.theta}, rho={request.rho}")
         await pattern_manager.move_polar(request.theta, request.rho)

+ 2 - 3
modules/core/pattern_manager.py

@@ -1406,9 +1406,8 @@ async def move_polar(theta, rho, speed=None):
         rho (float): Target rho coordinate
         speed (int, optional): Speed override. If None, uses state.speed
     """
-    # Clear stop_requested to ensure manual moves work after pattern stop
-    # Without this, moves would silently abort if stop_requested was left True
-    state.stop_requested = False
+    # Note: stop_requested is cleared once at pattern start (execute_theta_rho_file line 890)
+    # Don't clear it here on every coordinate - causes performance issues with event system
 
     # Ensure motion control thread is running
     if not motion_controller.running:

+ 46 - 10
modules/core/state.py

@@ -289,11 +289,29 @@ class AppState:
     def stop_requested(self, value: bool):
         self._stop_requested = value
         self._ensure_events()
-        if self._stop_event:
-            if value:
-                self._stop_event.set()
-            else:
-                self._stop_event.clear()
+        if self._stop_event and self._event_loop:
+            # asyncio.Event.set()/clear() are NOT thread-safe
+            # Use call_soon_threadsafe when called from non-async threads (e.g., motion thread)
+            try:
+                if asyncio.get_running_loop() == self._event_loop:
+                    # Same loop - safe to call directly
+                    if value:
+                        self._stop_event.set()
+                    else:
+                        self._stop_event.clear()
+                else:
+                    # Different loop - use thread-safe call
+                    if value:
+                        self._event_loop.call_soon_threadsafe(self._stop_event.set)
+                    else:
+                        self._event_loop.call_soon_threadsafe(self._stop_event.clear)
+            except RuntimeError:
+                # No running loop (sync context) - use thread-safe call
+                if self._event_loop.is_running():
+                    if value:
+                        self._event_loop.call_soon_threadsafe(self._stop_event.set)
+                    else:
+                        self._event_loop.call_soon_threadsafe(self._stop_event.clear)
 
     @property
     def skip_requested(self) -> bool:
@@ -303,11 +321,29 @@ class AppState:
     def skip_requested(self, value: bool):
         self._skip_requested = value
         self._ensure_events()
-        if self._skip_event:
-            if value:
-                self._skip_event.set()
-            else:
-                self._skip_event.clear()
+        if self._skip_event and self._event_loop:
+            # asyncio.Event.set()/clear() are NOT thread-safe
+            # Use call_soon_threadsafe when called from non-async threads (e.g., motion thread)
+            try:
+                if asyncio.get_running_loop() == self._event_loop:
+                    # Same loop - safe to call directly
+                    if value:
+                        self._skip_event.set()
+                    else:
+                        self._skip_event.clear()
+                else:
+                    # Different loop - use thread-safe call
+                    if value:
+                        self._event_loop.call_soon_threadsafe(self._skip_event.set)
+                    else:
+                        self._event_loop.call_soon_threadsafe(self._skip_event.clear)
+            except RuntimeError:
+                # No running loop (sync context) - use thread-safe call
+                if self._event_loop.is_running():
+                    if value:
+                        self._event_loop.call_soon_threadsafe(self._skip_event.set)
+                    else:
+                        self._event_loop.call_soon_threadsafe(self._skip_event.clear)
 
     def get_stop_event(self) -> Optional[asyncio.Event]:
         """Get the stop event for async waiting. Returns None if no event loop."""