Просмотр исходного кода

fix(printers): the print slot labels on AMS slots stay on when Bambuddy restarts during a print (the mapping captured from the print command is restored with the print session)

maziggy 21 часов назад
Родитель
Сommit
b63189f2be

+ 1 - 1
CHANGELOG.md

@@ -317,7 +317,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Stopping drying says the command was sent** — The toast said "Drying stopped" as soon as the command went out; it now says "Stop command sent".
 - **Stopping drying says the command was sent** — The toast said "Drying stopped" as soon as the command went out; it now says "Stop command sent".
 - **A skip the printer refuses or never applies is logged** — Skipped objects were marked skipped the moment the command was sent. A refusal from the printer now takes them back off the skipped list and is logged with the printer's reason, and a skip the printer has not listed after a minute is logged as unconfirmed.
 - **A skip the printer refuses or never applies is logged** — Skipped objects were marked skipped the moment the command was sent. A refusal from the printer now takes them back off the skipped list and is logged with the printer's reason, and a skip the printer has not listed after a minute is logged as unconfirmed.
 - **The printer card shows a nozzle change, firmware update or developer-mode switch at once** — These were left out of the live updates and only reached the card with the 30-second refresh or a page reload.
 - **The printer card shows a nozzle change, firmware update or developer-mode switch at once** — These were left out of the live updates and only reached the card with the 30-second refresh or a page reload.
-- **The print slot labels on AMS slots show up** — While a print runs, each AMS slot the print uses is meant to carry a small "P1 / P2" label, so a job sent to a printer with the wrong filament stands out. The labels read the print's mapping from the printer's own status, which almost never carries it, so they rarely appeared. Bambuddy already records the mapping from the print command, and the labels now use it. A print started from the printer's screen after a job that never started (refused, or cancelled before printing) no longer inherits that job's mapping, which usage tracking read as well.
+- **The print slot labels on AMS slots show up** — While a print runs, each AMS slot the print uses is meant to carry a small "P1 / P2" label, so a job sent to a printer with the wrong filament stands out. The labels read the print's mapping from the printer's own status, which almost never carries it, so they rarely appeared. Bambuddy already records the mapping from the print command, and the labels now use it. They stay on when Bambuddy is restarted while the print runs. A print started from the printer's screen after a job that never started (refused, or cancelled before printing) no longer inherits that job's mapping, which usage tracking read as well.
 - **Drying no longer offers spool rotation on an AMS with filament in the feed tube** — **Rotate spool during drying** was disabled only for the tray state that means "loaded" on some firmware, so on others it stayed available and the printer refused the drying start. The slot the printer reports as loaded, and on two-nozzle printers the slot each nozzle is fed from, now count too.
 - **Drying no longer offers spool rotation on an AMS with filament in the feed tube** — **Rotate spool during drying** was disabled only for the tray state that means "loaded" on some firmware, so on others it stayed available and the printer refused the drying start. The slot the printer reports as loaded, and on two-nozzle printers the slot each nozzle is fed from, now count too.
 - **The run-out hint names the slot on two-nozzle printers with several AMS** — When a print paused for filament, the hint often said to check the printer screen, because the slot number alone does not say which AMS it is on. The AMS wired to the active nozzle now settles it; with the Filament Track Switch, where an AMS can feed either nozzle, the hint still asks.
 - **The run-out hint names the slot on two-nozzle printers with several AMS** — When a print paused for filament, the hint often said to check the printer screen, because the slot number alone does not say which AMS it is on. The AMS wired to the active nozzle now settles it; with the Filament Track Switch, where an AMS can feed either nozzle, the hint still asks.
 - **A regular AMS is never shown as an AMS-HT** — An AMS was taken for an AMS-HT when it reported a single tray. The unit number (128-135 for AMS-HT) now decides, on the printer card and its slot labels ("HT-B" became "AMS-B"), in the AMS Filament Backup view, print mapping, the slicer's loaded-spool list and the queue. A regular AMS that reports only some of its trays shows each one in its own slot rather than in list order.
 - **A regular AMS is never shown as an AMS-HT** — An AMS was taken for an AMS-HT when it reported a single tray. The unit number (128-135 for AMS-HT) now decides, on the printer card and its slot labels ("HT-B" became "AMS-B"), in the AMS Filament Backup view, print mapping, the slicer's loaded-spool list and the queue. A regular AMS that reports only some of its trays shows each one in its own slot rather than in list order.

+ 6 - 0
backend/app/main.py

@@ -6797,6 +6797,7 @@ async def _restore_usage_tracking_session(printer_id: int, state, db, logger) ->
         from backend.app.api.routes.settings import get_setting
         from backend.app.api.routes.settings import get_setting
         from backend.app.services.usage_tracker import (
         from backend.app.services.usage_tracker import (
             clear_persisted_session,
             clear_persisted_session,
+            get_persisted_ams_mapping,
             get_persisted_print_name,
             get_persisted_print_name,
             restore_session,
             restore_session,
         )
         )
@@ -6824,6 +6825,11 @@ async def _restore_usage_tracking_session(printer_id: int, state, db, logger) ->
                 printer_id,
                 printer_id,
                 register_active=not (bool(_spoolman_on) and _spoolman_on.lower() == "true"),
                 register_active=not (bool(_spoolman_on) and _spoolman_on.lower() == "true"),
             )
             )
+            # The slot pills (#1762) read the mapping captured from the print
+            # command, which dies with the process as well. One this process
+            # has captured since is newer.
+            if state.print_ams_mapping is None:
+                state.print_ams_mapping = await get_persisted_ams_mapping(db, printer_id)
         if persisted_log:
         if persisted_log:
             restored = [tuple(entry) for entry in persisted_log if isinstance(entry, (list, tuple)) and len(entry) == 2]
             restored = [tuple(entry) for entry in persisted_log if isinstance(entry, (list, tuple)) and len(entry) == 2]
             # Anything this process already observed goes after the persisted
             # Anything this process already observed goes after the persisted

+ 8 - 0
backend/app/services/usage_tracker.py

@@ -419,6 +419,14 @@ async def get_persisted_print_name(db: AsyncSession, printer_id: int) -> str | N
     return row.print_name if row is not None else None
     return row.print_name if row is not None else None
 
 
 
 
+async def get_persisted_ams_mapping(db: AsyncSession, printer_id: int) -> list[int] | None:
+    """AMS mapping on the persisted row: the one captured from the print command."""
+    from backend.app.models.active_print_session import ActivePrintSession
+
+    row = await db.get(ActivePrintSession, printer_id)
+    return list(row.ams_mapping) if row is not None and row.ams_mapping else None
+
+
 async def restore_session(db: AsyncSession, printer_id: int, register_active: bool = True) -> list[list[int]] | None:
 async def restore_session(db: AsyncSession, printer_id: int, register_active: bool = True) -> list[list[int]] | None:
     """Rebuild the in-memory session for ``printer_id`` from the persisted row.
     """Rebuild the in-memory session for ``printer_id`` from the persisted row.
 
 

+ 35 - 0
backend/tests/unit/test_usage_tracker_restart_recovery.py

@@ -624,6 +624,7 @@ class TestRestoreOnRestartRecovery:
             tray_now=3,
             tray_now=3,
             layer_num=700,
             layer_num=700,
             last_loaded_tray=-1,
             last_loaded_tray=-1,
+            print_ams_mapping=None,
         )
         )
         for key, value in overrides.items():
         for key, value in overrides.items():
             setattr(state, key, value)
             setattr(state, key, value)
@@ -653,6 +654,36 @@ class TestRestoreOnRestartRecovery:
         assert state.tray_change_log == [(2, 0)]
         assert state.tray_change_log == [(2, 0)]
         assert _active_sessions[printer.id].plate_id == 1
         assert _active_sessions[printer.id].plate_id == 1
 
 
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("spoolman", [False, True])
+    async def test_the_print_s_slot_mapping_comes_back_onto_the_printer_state(self, db_session, printer, spoolman):
+        """The slot pills (#1762) read the mapping captured from the print
+        command, which dies with the process; with Spoolman too, which gets no
+        in-memory session."""
+        from backend.app.main import _restore_usage_tracking_session
+        from backend.app.models.settings import Settings
+
+        if spoolman:
+            db_session.add(Settings(key="spoolman_enabled", value="true"))
+            await db_session.commit()
+        await persist_session(db_session, self._session(printer.id), [(2, 0)])
+        state = self._state()
+
+        await _restore_usage_tracking_session(printer.id, state, db_session, MagicMock())
+
+        assert state.print_ams_mapping == [2]
+
+    @pytest.mark.asyncio
+    async def test_a_mapping_this_process_captured_is_kept(self, db_session, printer):
+        from backend.app.main import _restore_usage_tracking_session
+
+        await persist_session(db_session, self._session(printer.id), [(2, 0)])
+        state = self._state(print_ams_mapping=[5, 10])
+
+        await _restore_usage_tracking_session(printer.id, state, db_session, MagicMock())
+
+        assert state.print_ams_mapping == [5, 10]
+
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     async def test_entries_seen_by_this_process_are_kept_after_the_persisted_ones(self, db_session, printer):
     async def test_entries_seen_by_this_process_are_kept_after_the_persisted_ones(self, db_session, printer):
         from backend.app.main import _restore_usage_tracking_session
         from backend.app.main import _restore_usage_tracking_session
@@ -676,6 +707,8 @@ class TestRestoreOnRestartRecovery:
 
 
         assert state.tray_change_log == [(3, 700)]
         assert state.tray_change_log == [(3, 700)]
         assert state.last_loaded_tray == 3
         assert state.last_loaded_tray == 3
+        # And no slot pills: nothing says how the print's filaments were mapped.
+        assert state.print_ams_mapping is None
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     async def test_unloaded_tray_seeds_nothing(self, db_session, printer):
     async def test_unloaded_tray_seeds_nothing(self, db_session, printer):
@@ -703,6 +736,8 @@ class TestRestoreOnRestartRecovery:
         assert await restore_session(db_session, printer.id) is None
         assert await restore_session(db_session, printer.id) is None
         # Still seeded, so the rest of the running print stays attributable.
         # Still seeded, so the rest of the running print stays attributable.
         assert state.tray_change_log == [(3, 700)]
         assert state.tray_change_log == [(3, 700)]
+        # Nor do its slot pills show on the running print.
+        assert state.print_ams_mapping is None
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     async def test_an_unloaded_tray_does_not_clobber_last_loaded_tray(self, db_session, printer):
     async def test_an_unloaded_tray_does_not_clobber_last_loaded_tray(self, db_session, printer):