test_orca_slot_filament_id_3216.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. """An Orca Cloud profile reaches the AMS slot with its own filament id (#3216).
  2. OrcaSlicer's "Sync filaments" resolves a slot to a preset by ``filament_id``
  3. alone. The Configure dialog used to look the id up in the browser and, when
  4. that came back empty, quietly sent the generic for the material -- so every
  5. Orca custom filament reached the slicer as "Generic <material>", and nothing
  6. in the log said why. The lookup now runs in ``configure`` and its outcome is
  7. reported back.
  8. The slot preset row also records the filament id it was written with, so the
  9. slot card can tell when OrcaSlicer's Device tab re-configured the slot.
  10. """
  11. from unittest.mock import AsyncMock, MagicMock, patch
  12. import pytest
  13. from httpx import AsyncClient
  14. from backend.app.services.slicer_filament_resolver import OrcaFilamentLookup
  15. ORCA_ID = "34d8f588-860b-5be1-bcbe-c0d46d96324b"
  16. def _client():
  17. client = MagicMock()
  18. client.ams_set_filament_setting.return_value = True
  19. client.extrusion_cali_sel.return_value = True
  20. client.request_status_update.return_value = True
  21. return client
  22. def _status(tray_info_idx: str = "", tray_type: str = ""):
  23. status = MagicMock()
  24. status.raw_data = {
  25. "ams": {
  26. "ams": [
  27. {"id": 0, "tray": [{"id": 2, "tray_info_idx": tray_info_idx, "tray_type": tray_type}]},
  28. ]
  29. }
  30. }
  31. return status
  32. async def _configure(async_client: AsyncClient, printer_id: int, **extra):
  33. params = {
  34. "tray_info_idx": "",
  35. "tray_type": "PLA",
  36. "tray_sub_brands": "XXXXX EXAMPLE PLA Example",
  37. "tray_color": "FCECD6FF",
  38. "nozzle_temp_min": 190,
  39. "nozzle_temp_max": 230,
  40. "orca_profile_id": ORCA_ID,
  41. }
  42. params.update(extra)
  43. return await async_client.post(f"/api/v1/printers/{printer_id}/slots/0/2/configure", params=params)
  44. class TestConfigureWithOrcaProfile:
  45. @pytest.mark.asyncio
  46. @pytest.mark.integration
  47. async def test_the_profiles_own_filament_id_goes_to_the_slot(self, async_client: AsyncClient, printer_factory):
  48. """The id OrcaSlicer itself sends from its Device tab: Pfc74047 in the
  49. reporter's log, where Bambuddy sent GFL99 for the same profile."""
  50. printer = await printer_factory(name="H2D")
  51. client = _client()
  52. lookup = AsyncMock(return_value=OrcaFilamentLookup("Pfc74047", source="own"))
  53. with (
  54. patch("backend.app.api.routes.printers.printer_manager") as pm,
  55. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  56. ):
  57. pm.get_client.return_value = client
  58. pm.get_status.return_value = _status()
  59. response = await _configure(async_client, printer.id)
  60. assert response.status_code == 200
  61. body = response.json()
  62. assert body["tray_info_idx"] == "Pfc74047"
  63. assert body["orca_fallback_reason"] == ""
  64. assert lookup.await_args.args[2] == ORCA_ID
  65. kwargs = client.ams_set_filament_setting.call_args.kwargs
  66. assert kwargs["tray_info_idx"] == "Pfc74047"
  67. # Never the profile UUID: it is foreign to the printer and to the slicer.
  68. assert ORCA_ID not in kwargs["setting_id"]
  69. @pytest.mark.asyncio
  70. @pytest.mark.integration
  71. async def test_no_id_falls_back_to_the_generic_and_says_so(self, async_client: AsyncClient, printer_factory):
  72. """Not the slot's previous filament: reusing it would put the previous
  73. spool's preset in front of the slicer for a profile the user just chose."""
  74. printer = await printer_factory(name="H2D")
  75. client = _client()
  76. lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="lookup_failed"))
  77. with (
  78. patch("backend.app.api.routes.printers.printer_manager") as pm,
  79. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  80. ):
  81. pm.get_client.return_value = client
  82. pm.get_status.return_value = _status("P4d64437", "PLA")
  83. response = await _configure(async_client, printer.id)
  84. assert response.status_code == 200
  85. body = response.json()
  86. assert body["tray_info_idx"] == "GFL99"
  87. assert body["orca_fallback_reason"] == "lookup_failed"
  88. assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "GFL99"
  89. @pytest.mark.asyncio
  90. @pytest.mark.integration
  91. async def test_an_explicit_filament_id_skips_the_lookup(self, async_client: AsyncClient, printer_factory):
  92. printer = await printer_factory(name="H2D")
  93. client = _client()
  94. lookup = AsyncMock()
  95. with (
  96. patch("backend.app.api.routes.printers.printer_manager") as pm,
  97. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  98. ):
  99. pm.get_client.return_value = client
  100. pm.get_status.return_value = _status()
  101. response = await _configure(async_client, printer.id, tray_info_idx="Pad3f856")
  102. assert response.status_code == 200
  103. lookup.assert_not_awaited()
  104. assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "Pad3f856"
  105. @pytest.mark.asyncio
  106. @pytest.mark.integration
  107. async def test_other_presets_report_no_fallback(self, async_client: AsyncClient, printer_factory):
  108. printer = await printer_factory(name="H2D")
  109. client = _client()
  110. with patch("backend.app.api.routes.printers.printer_manager") as pm:
  111. pm.get_client.return_value = client
  112. pm.get_status.return_value = _status()
  113. response = await _configure(async_client, printer.id, tray_info_idx="GFA00", orca_profile_id="")
  114. assert response.status_code == 200
  115. assert response.json()["orca_fallback_reason"] == ""
  116. assert response.json()["tray_info_idx"] == "GFA00"
  117. class TestOrcaFallbackKeepsTheDialogsGenerics:
  118. """The fallback the Configure dialog used to pick in the browser, kept when
  119. the lookup moved to the server: its table has Silk, PCTG, PP and PE."""
  120. @pytest.mark.asyncio
  121. @pytest.mark.integration
  122. @pytest.mark.parametrize(
  123. ("material", "expected"),
  124. [
  125. ("PLA SILK", "GFL96"),
  126. ("PCTG", "GFG97"),
  127. ("PP", "GFP97"),
  128. ("PETG-CF", "GFG98"),
  129. ("ASA-CF", "GFB98"),
  130. ("PLA+", "GFL99"),
  131. ],
  132. )
  133. async def test_material_generic(self, async_client: AsyncClient, printer_factory, material, expected):
  134. printer = await printer_factory(name="H2D")
  135. client = _client()
  136. lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="no_filament_id"))
  137. with (
  138. patch("backend.app.api.routes.printers.printer_manager") as pm,
  139. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  140. ):
  141. pm.get_client.return_value = client
  142. pm.get_status.return_value = _status()
  143. response = await _configure(async_client, printer.id, tray_type=material)
  144. assert response.status_code == 200
  145. assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == expected
  146. assert response.json()["orca_fallback_reason"] == "no_filament_id"
  147. @pytest.mark.asyncio
  148. @pytest.mark.integration
  149. async def test_an_unstorable_looked_up_id_is_refused(self, async_client: AsyncClient, printer_factory):
  150. """Same refusal as the #3003 guard: an 18-character PFUS would be
  151. stored truncated and resolve nowhere."""
  152. printer = await printer_factory(name="H2D")
  153. client = _client()
  154. lookup = AsyncMock(return_value=OrcaFilamentLookup("PFUS9ac902733670a9", source="own"))
  155. with (
  156. patch("backend.app.api.routes.printers.printer_manager") as pm,
  157. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  158. ):
  159. pm.get_client.return_value = client
  160. pm.get_status.return_value = _status()
  161. response = await _configure(async_client, printer.id)
  162. assert client.ams_set_filament_setting.call_args.kwargs["tray_info_idx"] == "GFL99"
  163. assert response.json()["orca_fallback_reason"] == "no_filament_id"
  164. @pytest.mark.asyncio
  165. @pytest.mark.integration
  166. async def test_a_kprofile_realignment_clears_the_warning(self, async_client: AsyncClient, printer_factory):
  167. """The slot ends up on the K-profile's own filament, not a generic."""
  168. printer = await printer_factory(name="H2D")
  169. client = _client()
  170. lookup = AsyncMock(return_value=OrcaFilamentLookup(reason="no_filament_id"))
  171. with (
  172. patch("backend.app.api.routes.printers.printer_manager") as pm,
  173. patch("backend.app.api.routes.printers.lookup_orca_filament_id", lookup),
  174. ):
  175. pm.get_client.return_value = client
  176. pm.get_status.return_value = _status()
  177. response = await _configure(async_client, printer.id, kprofile_filament_id="P4d64437")
  178. assert response.status_code == 200
  179. assert response.json()["tray_info_idx"] == "P4d64437"
  180. assert response.json()["orca_fallback_reason"] == ""
  181. class TestSlotPresetRecordsItsFilamentId:
  182. @pytest.mark.asyncio
  183. @pytest.mark.integration
  184. async def test_saved_and_returned(self, async_client: AsyncClient, printer_factory):
  185. printer = await printer_factory(name="H2D")
  186. url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
  187. response = await async_client.put(
  188. url,
  189. params={
  190. "preset_id": f"orca_{ORCA_ID}",
  191. "preset_name": "XXXXX EXAMPLE PLA Example",
  192. "preset_source": "orca_cloud",
  193. "tray_info_idx": "Pfc74047",
  194. },
  195. )
  196. assert response.status_code == 200
  197. assert response.json()["tray_info_idx"] == "Pfc74047"
  198. assert (await async_client.get(url)).json()["tray_info_idx"] == "Pfc74047"
  199. listed = (await async_client.get(f"/api/v1/printers/{printer.id}/slot-presets")).json()
  200. assert listed["2"]["tray_info_idx"] == "Pfc74047"
  201. @pytest.mark.asyncio
  202. @pytest.mark.integration
  203. async def test_a_save_without_one_clears_the_old_one(self, async_client: AsyncClient, printer_factory):
  204. """A stale id must not outlive the preset it came with: the slot card
  205. would compare the new preset against the old preset's filament."""
  206. printer = await printer_factory(name="H2D")
  207. url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
  208. await async_client.put(
  209. url, params={"preset_id": "GFSL99", "preset_name": "Generic PLA", "tray_info_idx": "GFL99"}
  210. )
  211. response = await async_client.put(
  212. url, params={"preset_id": "local_5", "preset_name": "Mine", "preset_source": "local"}
  213. )
  214. assert response.status_code == 200
  215. assert response.json()["tray_info_idx"] is None
  216. assert (await async_client.get(url)).json()["tray_info_idx"] is None
  217. @pytest.mark.asyncio
  218. @pytest.mark.integration
  219. async def test_an_overlong_id_is_dropped_not_refused(self, async_client: AsyncClient, printer_factory):
  220. """Refusing it would lose the preset save itself, which the dialog only
  221. logs; recording nothing keeps the row shown as before."""
  222. printer = await printer_factory(name="H2D")
  223. url = f"/api/v1/printers/{printer.id}/slot-presets/0/2"
  224. response = await async_client.put(url, params={"preset_id": "x", "preset_name": "X", "tray_info_idx": "P" * 40})
  225. assert response.status_code == 200
  226. assert response.json()["tray_info_idx"] is None