Jelajahi Sumber

fix(drying): don't stop a running AMS dry on an unreliable humidity re-check (#1892)

Auto-drying stopped manually started (and pre-restart) AMS drying cycles
after exactly 30 minutes. The already-drying branch in _check_auto_drying()
applied a humidity-based auto-stop despite its own "track but don't stop"
comment, and the humidity re-check is unreliable: RH drops steeply in heated
air, so the sensor reads ~15-20% within minutes of the dryer starting even
with saturated filament. humidity <= threshold was thus effectively always
true, and the _min_drying_seconds=1800 floor pinned the stop to the 30-minute
mark. This also truncated Bambuddy's own preset-duration dries.

Remove the humidity-based early-stop entirely: a running dry now runs to its
configured duration (firmware stops it). Scheduling stops (print priority,
queue no longer needing the dry) are unchanged via _stop_drying(). Drop the
now-unused _min_drying_seconds.
maziggy 2 bulan lalu
induk
melakukan
53ae5fb620

File diff ditekan karena terlalu besar
+ 0 - 0
CHANGELOG.md


+ 20 - 25
backend/app/services/print_scheduler.py

@@ -165,7 +165,6 @@ class PrintScheduler:
         self._check_interval = 30  # seconds
         self._power_on_wait_time = 180  # seconds to wait for printer after power on (3 min)
         self._power_on_check_interval = 10  # seconds between connection checks
-        self._min_drying_seconds = 1800  # 30 minutes minimum before humidity re-check can stop drying
         # Track which printers are currently auto-drying (printer_id -> start timestamp)
         self._drying_in_progress: dict[int, float] = {}
         # Defensive in-memory dispatch hold (#1157): a printer that just received
@@ -1820,33 +1819,29 @@ class PrintScheduler:
                             humidity = int(h_idx)
                         except (ValueError, TypeError):
                             pass
-                # Already drying — check if humidity dropped below threshold (with minimum drying time)
+                # Already drying — let it run to its configured duration (#1892).
+                #
+                # We deliberately do NOT stop drying from a humidity re-check here.
+                # Relative humidity drops steeply in heated air, so the AMS sensor
+                # reads ~15-20% within minutes of the dryer starting even while the
+                # filament is still saturated. A humidity-based early-stop therefore
+                # always fires at the minimum-time floor, truncating both user-started
+                # manual cycles and Bambuddy's own preset-duration dries to ~30 min.
+                # The firmware stops when the configured duration elapses; scheduling
+                # stops (print takes priority, queue no longer needs drying) are
+                # handled separately via _stop_drying().
                 if dry_time > 0:
                     if pid not in self._drying_in_progress:
-                        # Drying we didn't start (manual or from before restart) — track but don't stop
+                        # Drying we didn't start (manual or from before restart) —
+                        # track it so scheduling stops still apply; never auto-stop it.
                         self._drying_in_progress[pid] = time.monotonic()
-                    started_at = self._drying_in_progress[pid]
-                    elapsed = time.monotonic() - started_at
-                    if humidity is not None and humidity <= humidity_threshold and elapsed >= self._min_drying_seconds:
-                        logger.info(
-                            "Auto-drying: printer %d AMS %d — humidity %d%% <= threshold %d%% after %dm, stopping drying",
-                            pid,
-                            ams_id,
-                            humidity,
-                            humidity_threshold,
-                            int(elapsed / 60),
-                        )
-                        printer_manager.send_drying_command(pid, ams_id, temp=0, duration=0, mode=0)
-                    else:
-                        logger.debug(
-                            "Auto-drying: printer %d AMS %d — drying (%dm left, humidity %s%%, elapsed %dm/%dm min)",
-                            pid,
-                            ams_id,
-                            dry_time,
-                            humidity,
-                            int(elapsed / 60),
-                            self._min_drying_seconds // 60,
-                        )
+                    logger.debug(
+                        "Auto-drying: printer %d AMS %d — drying (%dm left, humidity %s%%), letting it run",
+                        pid,
+                        ams_id,
+                        dry_time,
+                        humidity,
+                    )
                     continue
 
                 # Humidity below threshold — no need to start drying

+ 27 - 18
backend/tests/unit/test_scheduler_auto_drying.py

@@ -238,19 +238,24 @@ class TestStopDrying:
 
 
 class TestMinimumDryingTime:
-    """Regression: drying should not stop/restart rapidly when humidity oscillates near threshold."""
+    """Regression #1892: a running drying cycle must never be stopped by a humidity re-check.
+
+    Relative humidity reads low in heated air (the AMS sensor sees ~15-20% within
+    minutes of the dryer starting even while the filament is still saturated), so a
+    humidity-based auto-stop would truncate every cycle — manual or Bambuddy-started —
+    to the old minimum-time floor. Drying is now left to run to its configured
+    duration; the firmware stops it when the duration elapses.
+    """
 
     @pytest.fixture
     def scheduler(self):
-        s = PrintScheduler()
-        s._min_drying_seconds = 1800  # 30 minutes
-        return s
+        return PrintScheduler()
 
     @pytest.mark.asyncio
     @patch("backend.app.services.print_scheduler.printer_manager")
     @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
     async def test_no_stop_before_minimum_time(self, mock_sd, mock_pm, scheduler):
-        """Drying should NOT stop when humidity drops below threshold before 30 min."""
+        """Drying should NOT stop when humidity drops below threshold shortly after start."""
         # Simulate: drying started 5 minutes ago
         scheduler._drying_in_progress = {1: time.monotonic() - 300}
 
@@ -305,9 +310,9 @@ class TestMinimumDryingTime:
     @pytest.mark.asyncio
     @patch("backend.app.services.print_scheduler.printer_manager")
     @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
-    async def test_stops_after_minimum_time(self, mock_sd, mock_pm, scheduler):
-        """Drying SHOULD stop when humidity below threshold AND 30 min elapsed."""
-        # Simulate: drying started 35 minutes ago
+    async def test_no_stop_after_long_elapsed_time(self, mock_sd, mock_pm, scheduler):
+        """#1892: drying must NOT stop even long after start with low humidity — let it run."""
+        # Simulate: drying started 35 minutes ago, humidity reads low (heated air)
         scheduler._drying_in_progress = {1: time.monotonic() - 2100}
 
         state = MagicMock()
@@ -346,8 +351,11 @@ class TestMinimumDryingTime:
 
         await scheduler._check_auto_drying(db, [item], set())
 
-        # Should have sent stop command (humidity-based stop after minimum time)
-        mock_pm.send_drying_command.assert_any_call(1, 0, temp=0, duration=0, mode=0)
+        # Must NOT send a humidity-based stop — drying is left to run to its duration
+        for call in mock_pm.send_drying_command.call_args_list:
+            assert call != ((1, 0), {"temp": 0, "duration": 0, "mode": 0}), (
+                "Humidity re-check must never stop a running drying cycle (#1892)"
+            )
 
     @staticmethod
     def _make_setting(value):
@@ -783,19 +791,17 @@ class TestAmbientDrying(_DryingTestBase):
 
 
 class TestBlockForDryingBugFix(_DryingTestBase):
-    """Regression: block mode should not skip humidity auto-stop for already-drying printers."""
+    """Regression: block mode gates NEW drying starts but must leave running dries alone (#1892)."""
 
     @pytest.fixture
     def scheduler(self):
-        s = PrintScheduler()
-        s._min_drying_seconds = 1800
-        return s
+        return PrintScheduler()
 
     @pytest.mark.asyncio
     @patch("backend.app.services.print_scheduler.printer_manager")
     @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
-    async def test_block_mode_allows_humidity_stop_for_active_drying(self, mock_sd, mock_pm, scheduler):
-        """Bug fix: printer already drying in block mode should still check humidity to auto-stop."""
+    async def test_block_mode_leaves_active_drying_running(self, mock_sd, mock_pm, scheduler):
+        """#1892: a printer already drying in block mode must not be stopped by a humidity re-check."""
         # Drying started 35 minutes ago
         scheduler._drying_in_progress = {1: time.monotonic() - 2100}
 
@@ -837,8 +843,11 @@ class TestBlockForDryingBugFix(_DryingTestBase):
 
         await scheduler._check_auto_drying(db, [item], set())
 
-        # Should have sent stop command — humidity dropped below threshold after 30+ min
-        mock_pm.send_drying_command.assert_any_call(1, 0, temp=0, duration=0, mode=0)
+        # Must NOT stop the running dry — block mode gates new starts, not active cycles
+        for call in mock_pm.send_drying_command.call_args_list:
+            assert call != ((1, 0), {"temp": 0, "duration": 0, "mode": 0}), (
+                "Block mode must not stop an already-running drying cycle (#1892)"
+            )
 
     @pytest.mark.asyncio
     @patch("backend.app.services.print_scheduler.printer_manager")

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini