test_a2l_captured_mapping.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """An A2L AMS-Lite tray in a captured print command keeps its global id.
  2. The firmware addresses the Lite as physical unit 16: a print command carries
  3. the LOCAL slot 0-3 in the flat ``ams_mapping`` and ``{ams_id: 16, slot_id: n}``
  4. in ``ams_mapping2`` (confirmed against the firmware's own mapping; our dispatch
  5. sends the same). Inside Bambuddy the Lite is unit 6, global trays 24-27.
  6. The captured mapping kept the local slot, so a print from Lite slot 2 (global
  7. 26) reached usage tracking as tray 2 -- AMS 0 slot 3, or nothing on a Lite-only
  8. A2L -- and the Lite spool was never charged. The virtual printer took the
  9. slicer's mapping through the same resolver, so a queued A2L job dispatched to
  10. AMS 0 instead of the Lite.
  11. """
  12. import json
  13. from unittest.mock import MagicMock
  14. import pytest
  15. from backend.app.services.bambu_mqtt import BambuMQTTClient, resolve_external_spools_in_mapping
  16. from backend.app.services.virtual_printer.manager import _extract_slicer_ams_mapping_json
  17. pytestmark = pytest.mark.unit
  18. def lite(slot: int) -> dict:
  19. return {"ams_id": 16, "slot_id": slot}
  20. EXT_RIGHT = {"ams_id": 255, "slot_id": 0}
  21. UNMAPPED = {"ams_id": 255, "slot_id": 255}
  22. def a2l_client() -> BambuMQTTClient:
  23. client = BambuMQTTClient(ip_address="10.0.0.9", serial_number="A2L0001", access_code="12345678", model="N9")
  24. client._client = MagicMock()
  25. client.state.connected = True
  26. return client
  27. class TestTheResolver:
  28. def test_lite_slot_becomes_its_global_tray(self):
  29. assert resolve_external_spools_in_mapping([1], [lite(1)], is_dual_nozzle=False) == [25]
  30. def test_every_lite_slot(self):
  31. mapping2 = [lite(0), lite(1), lite(2), lite(3)]
  32. assert resolve_external_spools_in_mapping([0, 1, 2, 3], mapping2, is_dual_nozzle=False) == [24, 25, 26, 27]
  33. def test_regular_ams_beside_the_lite_is_untouched(self):
  34. mapping2 = [{"ams_id": 0, "slot_id": 1}, lite(1), EXT_RIGHT, UNMAPPED]
  35. assert resolve_external_spools_in_mapping([1, 1, -1, -1], mapping2, is_dual_nozzle=False) == [1, 25, 254, -1]
  36. def test_only_a_minus_one_becomes_an_external_spool(self):
  37. """The Lite branch moved the external-spool guard; a real flat tray
  38. must still win over an ams_mapping2 entry that names the external spool."""
  39. assert resolve_external_spools_in_mapping([3], [EXT_RIGHT], is_dual_nozzle=False) == [3]
  40. def test_slots_outside_the_lite_stay_as_captured(self):
  41. assert resolve_external_spools_in_mapping([1, -1], [lite(4), lite(255)], is_dual_nozzle=False) == [1, -1]
  42. def test_without_mapping2_the_flat_list_stays(self):
  43. # Nothing names the unit, so there is nothing to resolve from.
  44. assert resolve_external_spools_in_mapping([1], None, is_dual_nozzle=False) == [1]
  45. class TestTheCapture:
  46. def test_slicer_command_captures_the_global_tray(self):
  47. client = a2l_client()
  48. client._handle_request_message(
  49. {"print": {"command": "project_file", "url": "ftp://x.3mf", "ams_mapping": [2], "ams_mapping2": [lite(2)]}}
  50. )
  51. assert client._captured_ams_mapping == [26]
  52. def test_our_own_dispatch_round_trips(self):
  53. """What we send for global 25 must be captured back as 25."""
  54. client = a2l_client()
  55. assert client.start_print("lite.3mf", ams_mapping=[25], use_ams=True) is True
  56. sent = json.loads(client._client.publish.call_args.args[1])
  57. assert sent["print"]["ams_mapping"] == [1]
  58. assert sent["print"]["ams_mapping2"] == [lite(1)]
  59. client._handle_request_message(sent)
  60. assert client._captured_ams_mapping == [25]
  61. def test_report_topic_echo_captures_the_global_tray(self):
  62. """The printer's own project_file response goes through the same capture."""
  63. client = a2l_client()
  64. client._process_message(
  65. {
  66. "print": {
  67. "command": "project_file",
  68. "result": "SUCCESS",
  69. "url": "file:///userdata/model/history/lite.gcode.3mf",
  70. "ams_mapping": [0],
  71. "ams_mapping2": [lite(0)],
  72. }
  73. }
  74. )
  75. assert client._captured_ams_mapping == [24]
  76. class TestTheVirtualPrinterIntake:
  77. def test_slicer_lite_pick_is_stored_as_the_global_tray(self):
  78. data = {"ams_mapping": [3], "ams_mapping2": [lite(3)]}
  79. assert json.loads(_extract_slicer_ams_mapping_json(data, "[VP test]")) == [27]