|
@@ -0,0 +1,121 @@
|
|
|
|
|
+"""Progress milestones count from the first layer, not from the start of preparation (#3211).
|
|
|
|
|
+
|
|
|
|
|
+An A1 mini (firmware 01.07.01.00) reports ``mc_percent`` 85 in the first frame
|
|
|
|
|
+of a print, while still preheating the bed at layer 0. Through its calibration
|
|
|
|
|
+the value then reads 3, 7, 40 and 44, and the first real layer starts at 45.
|
|
|
|
|
+Read as progress, the 85 sent the 75% notification together with Print
|
|
|
|
|
+Started -- with "Unknown" remaining time, since that frame reports 0 -- and
|
|
|
|
|
+with 75 then on record, 25 and 50 could never fire.
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+from types import SimpleNamespace
|
|
|
|
|
+from unittest.mock import MagicMock, patch
|
|
|
|
|
+
|
|
|
|
|
+import pytest
|
|
|
|
|
+
|
|
|
|
|
+from backend.app import main as main_module
|
|
|
|
|
+
|
|
|
|
|
+pytestmark = pytest.mark.unit
|
|
|
|
|
+
|
|
|
|
|
+PRINTER = 1
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.fixture(autouse=True)
|
|
|
|
|
+def _clean_milestones():
|
|
|
|
|
+ main_module._last_progress_milestone.clear()
|
|
|
|
|
+ yield
|
|
|
|
|
+ main_module._last_progress_milestone.clear()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _frame(progress: float, layer_num: int, total_layers: int = 128, stg_cur: int = 0) -> SimpleNamespace:
|
|
|
|
|
+ return SimpleNamespace(progress=progress, layer_num=layer_num, total_layers=total_layers, stg_cur=stg_cur)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _run(frames) -> list[int]:
|
|
|
|
|
+ sent = []
|
|
|
|
|
+ for progress, layer, *rest in frames:
|
|
|
|
|
+ milestone = main_module._progress_milestone_to_notify(PRINTER, _frame(progress, layer, *rest))
|
|
|
|
|
+ if milestone is not None:
|
|
|
|
|
+ sent.append(milestone)
|
|
|
|
|
+ return sent
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# The reporter's print, frame by frame from the support bundle: preparation at
|
|
|
|
|
+# layer 0, then the print proper from layer 1.
|
|
|
|
|
+A1_MINI_PRINT = [
|
|
|
|
|
+ (85, 0),
|
|
|
|
|
+ (3, 0),
|
|
|
|
|
+ (7, 0),
|
|
|
|
|
+ (40, 0),
|
|
|
|
|
+ (44, 0),
|
|
|
|
|
+ (45, 1),
|
|
|
|
|
+ (50, 1),
|
|
|
|
|
+ (52, 2),
|
|
|
|
|
+ (75, 30),
|
|
|
|
|
+ (97, 120),
|
|
|
|
|
+ (100, 128),
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_the_reported_print_sends_each_milestone_once_and_none_at_the_start():
|
|
|
|
|
+ assert _run(A1_MINI_PRINT) == [25, 50, 75]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_nothing_is_sent_during_preparation():
|
|
|
|
|
+ assert _run(A1_MINI_PRINT[:5]) == []
|
|
|
|
|
+ assert main_module._last_progress_milestone.get(PRINTER, 0) == 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_the_first_layer_catches_up_to_the_milestone_already_passed():
|
|
|
|
|
+ """The print proper starts at 45 here, so 25 goes out at 45 -- once."""
|
|
|
|
|
+ assert _run([(85, 0), (45, 1), (46, 1)]) == [25]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_a_printer_without_layer_data_keeps_the_old_behaviour():
|
|
|
|
|
+ """No layer count means nothing to gate on; never notifying would be worse."""
|
|
|
|
|
+ assert _run([(10, 0, 0), (30, 0, 0), (60, 0, 0), (80, 0, 0)]) == [25, 50, 75]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_without_a_layer_count_the_preparation_stage_holds_it_back():
|
|
|
|
|
+ """A first frame with no total_layer_num leaves nothing to gate on by layers.
|
|
|
|
|
+
|
|
|
|
|
+ The A1 mini's preparation stages, from the bundle: 2 (bed preheating),
|
|
|
|
|
+ 4, 14 and 1, then 0 from the first layer. Until the pushall brings the
|
|
|
|
|
+ total, the stage is what tells preparation from printing.
|
|
|
|
|
+ """
|
|
|
|
|
+ frames = [(85, 0, 0, 2), (3, 0, 0, 4), (40, 0, 0, 14), (44, 0, 0, 1), (45, 1, 128, 0), (50, 2, 128, 0)]
|
|
|
|
|
+ assert _run(frames) == [25, 50]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.parametrize("no_stage", [-1, 255])
|
|
|
|
|
+def test_no_stage_at_all_is_not_preparation(no_stage):
|
|
|
|
|
+ assert _run([(30, 0, 0, no_stage)]) == [25]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_a_normal_print_is_unchanged():
|
|
|
|
|
+ frames = [(1, 1), (24, 10), (25, 11), (49, 20), (50, 21), (74, 30), (75, 31), (100, 40)]
|
|
|
|
|
+ assert _run(frames) == [25, 50, 75]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
|
+async def test_a_new_print_starts_its_milestones_from_zero():
|
|
|
|
|
+ """A printer can go from FINISH at 100% straight into the next print.
|
|
|
|
|
+
|
|
|
|
|
+ The status path only resets on progress below 5 while not printing, which
|
|
|
|
|
+ that never shows, so the previous print's 75 would block every milestone
|
|
|
|
|
+ of the next one. The print-start callback resets it.
|
|
|
|
|
+ """
|
|
|
|
|
+ assert _run(A1_MINI_PRINT) == [25, 50, 75]
|
|
|
|
|
+
|
|
|
|
|
+ class _Stop(Exception):
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # The reset is one of the first things on_print_start does; the rest of it
|
|
|
|
|
+ # (archiving, usage tracking, notifications) is not under test here, so the
|
|
|
|
|
+ # call is stopped at the next statement.
|
|
|
|
|
+ stop_here = SimpleNamespace(pop=MagicMock(side_effect=_Stop))
|
|
|
|
|
+ with patch.object(main_module, "_kill_switch_notification_tasks", stop_here), pytest.raises(_Stop):
|
|
|
|
|
+ await main_module.on_print_start(PRINTER, {})
|
|
|
|
|
+
|
|
|
|
|
+ assert main_module._last_progress_milestone[PRINTER] == 0
|
|
|
|
|
+ assert _run(A1_MINI_PRINT) == [25, 50, 75]
|