drying_preflight.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Shared checks run before an AMS drying command is sent.
  2. Both the immediate POST /printers/{id}/drying/start endpoint and the
  3. scheduler's delayed dispatch go through here, so a run that the immediate
  4. path would refuse is never silently published by the scheduled path.
  5. """
  6. from backend.app.services.printer_manager import drying_screen_only, supports_drying
  7. SCREEN_ONLY_DETAIL = "This printer only supports AMS drying from its own screen"
  8. UNSUPPORTED_DETAIL = "Drying not supported for this printer model or firmware version"
  9. # Codes stored on a failed scheduled drying (ScheduledDrying.error_code) so the
  10. # UI can show the reason translated. check_drying_supported() returns one of the
  11. # two details above; DETAIL_CODES maps it to its code.
  12. DID_NOT_START_DETAIL = "The printer accepted the drying command, but the AMS did not start drying"
  13. DETAIL_CODES = {
  14. SCREEN_ONLY_DETAIL: "screen_only",
  15. UNSUPPORTED_DETAIL: "unsupported",
  16. DID_NOT_START_DETAIL: "did_not_start",
  17. }
  18. # Firmware dry_sf_reason codes, as surfaced by the AMS status payload.
  19. DRY_SF_REASON_MESSAGES = {
  20. 0: "Printer is busy",
  21. 1: "Insufficient power: too many AMS drying or external PSU required",
  22. 2: "AMS is busy",
  23. 3: "Filament is at the AMS outlet, retract it first",
  24. 4: "AMS is already starting a drying cycle",
  25. 5: "Not supported in 2D mode",
  26. 6: "AMS is already drying",
  27. 7: "AMS firmware is upgrading",
  28. 8: "Plug in the external AMS power adapter to start drying",
  29. }
  30. # Codes 1 and 8 mean a power-supply problem the user has to fix; the rest
  31. # clear on their own. The frontend splits blocked states the same way.
  32. POWER_REASON_CODES = frozenset({1, 8})
  33. # Code 3 also needs the user to act, but the fix is retracting filament rather
  34. # than anything to do with power, so it gets its own token instead of the
  35. # generic "cannot dry right now" the transient codes share.
  36. RETRACT_REASON_CODE = 3
  37. WAITING_REASON_POWER = "ams_power_required"
  38. WAITING_REASON_RETRACT = "ams_retract_filament"
  39. WAITING_REASON_BLOCKED = "ams_blocked"
  40. def check_drying_supported(model: str | None, firmware: str | None, *, require_firmware: bool = True) -> str | None:
  41. """Return a message if this printer cannot dry, else None.
  42. Pass require_firmware=False when there is no live status to read a version
  43. from, which leaves only the model check. Dispatch always has a status and
  44. judges both.
  45. """
  46. if drying_screen_only(model):
  47. return SCREEN_ONLY_DETAIL
  48. if require_firmware and not supports_drying(model, firmware):
  49. return UNSUPPORTED_DETAIL
  50. return None
  51. def find_ams_unit(state, ams_id: int) -> dict | None:
  52. """Locate an AMS unit in a live printer status payload."""
  53. for unit in (state.raw_data.get("ams") if state else None) or []:
  54. try:
  55. if int(unit.get("id", -1)) == ams_id:
  56. return unit
  57. except (TypeError, ValueError):
  58. continue
  59. return None
  60. def blocking_reason_codes(unit: dict | None) -> list[int]:
  61. """Known dry_sf_reason codes on this unit, in reported order."""
  62. codes = []
  63. for code in (unit or {}).get("dry_sf_reason") or []:
  64. try:
  65. code_int = int(code)
  66. except (TypeError, ValueError):
  67. continue
  68. if code_int in DRY_SF_REASON_MESSAGES:
  69. codes.append(code_int)
  70. return codes
  71. def primary_reason_code(codes: list[int]) -> int | None:
  72. """The one code to report when the AMS sets several at once.
  73. Power and filament-at-the-outlet need the user to go and do something; the
  74. rest clear on their own. Naming an actionable one is more use than whichever
  75. the firmware happened to list first, and going through here keeps the
  76. immediate endpoint's message and the scheduled row's waiting_reason
  77. describing the same blocked AMS the same way.
  78. """
  79. for code in codes:
  80. if code in POWER_REASON_CODES:
  81. return code
  82. if RETRACT_REASON_CODE in codes:
  83. return RETRACT_REASON_CODE
  84. return codes[0] if codes else None
  85. def waiting_reason_for_codes(codes: list[int]) -> str:
  86. """Map blocking codes onto the token the frontend translates."""
  87. code = primary_reason_code(codes)
  88. if code in POWER_REASON_CODES:
  89. return WAITING_REASON_POWER
  90. if code == RETRACT_REASON_CODE:
  91. return WAITING_REASON_RETRACT
  92. return WAITING_REASON_BLOCKED
  93. def resolve_filament(unit: dict | None, filament: str) -> str:
  94. """Fill an empty filament field from the first loaded tray.
  95. The printer rejects a drying payload with no filament type, so both
  96. callers fall back to the loaded spool and then to PLA.
  97. """
  98. if filament:
  99. return filament
  100. for tray in (unit or {}).get("tray") or []:
  101. tray_type = tray.get("tray_type")
  102. if tray_type:
  103. return str(tray_type)
  104. return "PLA"