Support non-0.4mm nozzles in AMS Slot config + guard dispatch (#1899)

The Configure AMS Slot picker was hardwired to 0.4mm (nozzleDiameter
prop never passed from PrintersPage / SpoolBuddyAmsPage), so a 0.6
machine could only set 0.4 profiles on its trays. Resolve the real
installed nozzle per-AMS (ams_extruder_map on dual-nozzle) and pass it
in. Separately, nothing validated the sliced nozzle against the
installed one, so a mismatch reached the printer as a cryptic HMS
_8012 "Failed to get AMS mapping table". Add a fail-safe pre-dispatch
guard in _start_print that fails the item with an actionable message
before upload; no slice diameter or no reported nozzles = no-op.
This commit is contained in:
maziggy
2026-07-08 08:57:56 +02:00
parent 55ea4b19c9
commit bb3e2a710e
9 changed files with 568 additions and 143 deletions
+85
View File
@@ -144,6 +144,50 @@ def _canonical_filament_type(ftype: str) -> str:
return _FILAMENT_EQUIV_MAP.get(upper, upper)
def _installed_nozzle_diameters(status) -> list[float]:
"""Parse the installed nozzle diameters from a PrinterState (#1899).
Returns the diameters the printer actually reports (e.g. [0.4] single-nozzle,
[0.4, 0.6] dual-nozzle), skipping the empty-string defaults that populate a
NozzleInfo before MQTT fills it in. An empty list means "the printer hasn't
told us its nozzle hardware" — callers must treat that as unknown, not as a
mismatch, so we never block a print on missing data.
"""
diameters: list[float] = []
for nozzle in getattr(status, "nozzles", None) or []:
raw = getattr(nozzle, "nozzle_diameter", "") or ""
try:
value = float(raw)
except (TypeError, ValueError):
continue
if value > 0:
diameters.append(value)
return diameters
def _nozzle_mismatch_message(sliced_nozzle: float | None, installed: list[float]) -> str | None:
"""Return an actionable error message when the sliced nozzle can't be
printed on any installed nozzle, else None (#1899).
Fail-safe: returns None whenever we lack the data to judge — no sliced
diameter, or the printer reported no nozzles — so a print is only ever
blocked on a POSITIVE mismatch. On dual-nozzle printers a match against
EITHER installed nozzle passes (a 0.6 slice is fine if one hotend is 0.6).
The 0.05 tolerance absorbs float noise while staying well inside the 0.2
gap between adjacent nozzle sizes (0.2/0.4/0.6/0.8).
"""
if not sliced_nozzle or not installed:
return None
if any(abs(d - sliced_nozzle) < 0.05 for d in installed):
return None
installed_str = " / ".join(f"{d:g}mm" for d in installed)
return (
f"File sliced for a {sliced_nozzle:g}mm nozzle, but the printer has "
f"{installed_str} installed. Re-slice for the installed nozzle, or "
f"install the matching nozzle before printing."
)
class PrintScheduler:
"""Background scheduler that processes the print queue."""
@@ -2629,6 +2673,47 @@ class PrintScheduler:
await self._power_off_if_needed(db, item)
return
# Nozzle-diameter mismatch guard (#1899). A file sliced for one nozzle
# size dispatched to a printer with a different nozzle installed is
# rejected by the firmware with a cryptic HMS ("Failed to get AMS mapping
# table" 0700_8012, or "nozzle diameter … not consistent" 0500_4038) that
# gives the user no idea what went wrong. Catch it here, before we spend
# time preheating and uploading, and fail with an actionable message.
# Fail-safe by construction: only a POSITIVE mismatch blocks — when the
# slice carries no nozzle diameter (archive.nozzle_diameter is None) or
# the printer hasn't reported its nozzles yet, we fall through and let the
# print proceed exactly as before. On dual-nozzle printers (H2D) a match
# against EITHER installed nozzle passes, so a 0.6 slice is fine as long
# as one of the two hotends is a 0.6.
sliced_nozzle = archive.nozzle_diameter if archive else None
if sliced_nozzle:
installed = _installed_nozzle_diameters(printer_manager.get_status(item.printer_id))
mismatch_msg = _nozzle_mismatch_message(sliced_nozzle, installed)
if mismatch_msg:
item.status = "failed"
item.error_message = mismatch_msg
item.completed_at = datetime.now(timezone.utc)
await db.commit()
logger.warning("Queue item %s: nozzle mismatch — %s", item.id, mismatch_msg)
await notification_service.on_queue_job_failed(
job_name=filename.replace(".gcode.3mf", "").replace(".3mf", ""),
printer_id=printer.id,
printer_name=printer.name,
reason=mismatch_msg,
db=db,
)
try:
await ws_manager.send_queue_item_failed(
user_id=item.created_by_id,
queue_item_id=item.id,
printer_id=item.printer_id,
reason="nozzle_mismatch",
)
except Exception:
pass
await self._power_off_if_needed(db, item)
return
# Preheat / heat-soak (#1468) — fires before upload so the printer's
# bed (and chamber, if applicable) is at temperature when the firmware
# starts the actual print routine. Best-effort: any failure logs and