|
@@ -5,6 +5,7 @@ import json
|
|
|
import logging
|
|
import logging
|
|
|
import time
|
|
import time
|
|
|
import uuid
|
|
import uuid
|
|
|
|
|
+import zipfile
|
|
|
from collections import deque
|
|
from collections import deque
|
|
|
from collections.abc import Mapping
|
|
from collections.abc import Mapping
|
|
|
from dataclasses import dataclass
|
|
from dataclasses import dataclass
|
|
@@ -74,8 +75,10 @@ from backend.app.utils.printer_models import (
|
|
|
normalize_printer_model,
|
|
normalize_printer_model,
|
|
|
)
|
|
)
|
|
|
from backend.app.utils.threemf_tools import (
|
|
from backend.app.utils.threemf_tools import (
|
|
|
|
|
+ default_plate_number,
|
|
|
extract_rack_plan_from_3mf,
|
|
extract_rack_plan_from_3mf,
|
|
|
extract_slot_extruders_from_3mf,
|
|
extract_slot_extruders_from_3mf,
|
|
|
|
|
+ select_plate_gcode_name,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
@@ -853,6 +856,77 @@ def _unmatched_filament_message(required: list[dict], loaded: list[dict]) -> str
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _effective_plate_id(explicit_plate_id: int | None, file_path: Path) -> int:
|
|
|
|
|
+ """The plate to dispatch, resolved once in ``_start_print`` and reused at
|
|
|
|
|
+ every call site below it: G-code injection, usage registration, rack-plan
|
|
|
|
|
+ lookup, slot-extruder lookup, the external-spool check, and the actual
|
|
|
|
|
+ print command.
|
|
|
|
|
+
|
|
|
|
|
+ A positive explicit ``plate_id`` on the queue item always wins. A
|
|
|
|
|
+ non-positive one is treated as "not set" and resolved from the archive,
|
|
|
|
|
+ the way the rest of the queue code already reads it (``if item.plate_id:``
|
|
|
|
|
+ in ``api/routes/print_queue.py``): the schemas put no lower bound on the
|
|
|
|
|
+ field, and passing a 0 straight through would build a print command for
|
|
|
|
|
+ ``Metadata/plate_0.gcode``, which is the same wedge this function exists
|
|
|
|
|
+ to prevent. The ``item.plate_id or 1`` this replaced mapped 0 to 1.
|
|
|
|
|
+
|
|
|
|
|
+ Falling back to a bare ``1`` instead of reading the archive assumes a
|
|
|
|
|
+ single-plate file's one G-code is numbered 1, which only holds for a
|
|
|
|
|
+ plate exported on its own: one cut out of a larger project keeps its
|
|
|
|
|
+ ORIGINAL plate number, so a printer asked to print "plate 1" of a file
|
|
|
|
|
+ whose only G-code is ``plate_2.gcode`` accepts the command, can't find
|
|
|
|
|
+ the file, throws an HMS error, and sits wedged in IDLE until
|
|
|
|
|
+ power-cycled (#2947).
|
|
|
|
|
+
|
|
|
|
|
+ The call sites agreed on a fallback only by accident before this:
|
|
|
|
|
+ with G-code injection on, ``inject_gcode_into_3mf`` already falls back to
|
|
|
|
|
+ the archive's own default plate internally whenever the plate id it's
|
|
|
|
|
+ handed isn't in the file, so it could silently inject into a different
|
|
|
|
|
+ plate than the one the print command itself asked for.
|
|
|
|
|
+
|
|
|
|
|
+ Falls back to 1 when the archive can't be read, holds no G-code member at
|
|
|
|
|
+ all, or its default member doesn't follow the ``plate_N`` naming
|
|
|
|
|
+ convention (a slicer that doesn't use it has no number to dispatch).
|
|
|
|
|
+
|
|
|
|
|
+ An explicit plate the archive doesn't hold is logged and then sent
|
|
|
|
|
+ anyway. It wedges the printer exactly like #2947 did, but redirecting it
|
|
|
|
|
+ to a plate that is in the file would print a model nobody asked for,
|
|
|
|
|
+ which is the worse of the two.
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ with zipfile.ZipFile(file_path, "r") as zf:
|
|
|
|
|
+ names = zf.namelist()
|
|
|
|
|
+ except (OSError, zipfile.BadZipFile) as exc:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Dispatch plate: cannot read %s (%s), so the archive's own plate numbering "
|
|
|
|
|
+ "is unavailable; dispatching plate %s",
|
|
|
|
|
+ file_path,
|
|
|
|
|
+ exc,
|
|
|
|
|
+ explicit_plate_id if explicit_plate_id is not None and explicit_plate_id > 0 else 1,
|
|
|
|
|
+ )
|
|
|
|
|
+ names = None
|
|
|
|
|
+
|
|
|
|
|
+ if explicit_plate_id is not None and explicit_plate_id > 0:
|
|
|
|
|
+ if (
|
|
|
|
|
+ names is not None
|
|
|
|
|
+ and default_plate_number(names) is not None
|
|
|
|
|
+ and select_plate_gcode_name(names, explicit_plate_id) is None
|
|
|
|
|
+ ):
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Dispatch plate: %s was queued for plate %s but holds no G-code for it "
|
|
|
|
|
+ "(it has %s). Sending plate %s as asked; expect the printer to reject the "
|
|
|
|
|
+ "file, since printing a different plate would print the wrong model (#2947)",
|
|
|
|
|
+ file_path,
|
|
|
|
|
+ explicit_plate_id,
|
|
|
|
|
+ ", ".join(sorted(n for n in names if n.endswith(".gcode"))),
|
|
|
|
|
+ explicit_plate_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ return explicit_plate_id
|
|
|
|
|
+
|
|
|
|
|
+ resolved = default_plate_number(names) if names is not None else None
|
|
|
|
|
+ return resolved if resolved is not None else 1
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class PrintScheduler:
|
|
class PrintScheduler:
|
|
|
"""Background scheduler that processes the print queue."""
|
|
"""Background scheduler that processes the print queue."""
|
|
|
|
|
|
|
@@ -6833,6 +6907,10 @@ class PrintScheduler:
|
|
|
logger.info("Queue item %s: dispatch abandoned — cancelled during preheat", item.id)
|
|
logger.info("Queue item %s: dispatch abandoned — cancelled during preheat", item.id)
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # See `_effective_plate_id` for why this is resolved once here rather
|
|
|
|
|
+ # than each site below repeating `item.plate_id or 1`.
|
|
|
|
|
+ effective_plate_id = _effective_plate_id(item.plate_id, file_path)
|
|
|
|
|
+
|
|
|
# G-code injection for auto-print systems (#422)
|
|
# G-code injection for auto-print systems (#422)
|
|
|
injected_path = None
|
|
injected_path = None
|
|
|
# #2547: tracked separately from `injected_path`, which is also set when
|
|
# #2547: tracked separately from `injected_path`, which is also set when
|
|
@@ -6851,7 +6929,7 @@ class PrintScheduler:
|
|
|
from backend.app.utils.threemf_tools import inject_gcode_into_3mf
|
|
from backend.app.utils.threemf_tools import inject_gcode_into_3mf
|
|
|
|
|
|
|
|
injected_path = inject_gcode_into_3mf(
|
|
injected_path = inject_gcode_into_3mf(
|
|
|
- file_path, item.plate_id or 1, start_gc or None, end_gc or None
|
|
|
|
|
|
|
+ file_path, effective_plate_id, start_gc or None, end_gc or None
|
|
|
)
|
|
)
|
|
|
if injected_path:
|
|
if injected_path:
|
|
|
file_path = injected_path
|
|
file_path = injected_path
|
|
@@ -7052,7 +7130,11 @@ class PrintScheduler:
|
|
|
ams_mapping=ams_mapping,
|
|
ams_mapping=ams_mapping,
|
|
|
created_by_id=item.created_by_id,
|
|
created_by_id=item.created_by_id,
|
|
|
cost_center_id=item.cost_center_id,
|
|
cost_center_id=item.cost_center_id,
|
|
|
- plate_id=item.plate_id,
|
|
|
|
|
|
|
+ # The plate actually dispatched, not the queue item's raw
|
|
|
|
|
+ # column: on None, `register_expected_print` stores nothing,
|
|
|
|
|
+ # and `extract_filament_usage_from_3mf` then books every
|
|
|
|
|
+ # filament in the file rather than the one plate that printed.
|
|
|
|
|
+ plate_id=effective_plate_id,
|
|
|
)
|
|
)
|
|
|
# Registration happens before the print command by necessity (the
|
|
# Registration happens before the print command by necessity (the
|
|
|
# printer can report the print before the send returns), so record
|
|
# printer can report the print before the send returns), so record
|
|
@@ -7209,7 +7291,7 @@ class PrintScheduler:
|
|
|
# rack as it stands right now, after the upload, not at queue time.
|
|
# rack as it stands right now, after the upload, not at queue time.
|
|
|
resolved_nozzle_mapping = None
|
|
resolved_nozzle_mapping = None
|
|
|
if not item.nozzle_mapping and file_path is not None and is_nozzle_rack_model(printer.model):
|
|
if not item.nozzle_mapping and file_path is not None and is_nozzle_rack_model(printer.model):
|
|
|
- rack_plan = extract_rack_plan_from_3mf(file_path, plate_id=item.plate_id or 1)
|
|
|
|
|
|
|
+ rack_plan = extract_rack_plan_from_3mf(file_path, plate_id=effective_plate_id)
|
|
|
if rack_plan is not None:
|
|
if rack_plan is not None:
|
|
|
try:
|
|
try:
|
|
|
stored_choice = json.loads(item.nozzle_rack_choice) if item.nozzle_rack_choice else {}
|
|
stored_choice = json.loads(item.nozzle_rack_choice) if item.nozzle_rack_choice else {}
|
|
@@ -7308,7 +7390,7 @@ class PrintScheduler:
|
|
|
and file_path is not None
|
|
and file_path is not None
|
|
|
and is_nozzle_rack_model(printer.model)
|
|
and is_nozzle_rack_model(printer.model)
|
|
|
):
|
|
):
|
|
|
- slot_extruders = extract_slot_extruders_from_3mf(file_path, plate_id=item.plate_id or 1)
|
|
|
|
|
|
|
+ slot_extruders = extract_slot_extruders_from_3mf(file_path, plate_id=effective_plate_id)
|
|
|
if slot_extruders:
|
|
if slot_extruders:
|
|
|
nozzle_slot_extruders = json.dumps(slot_extruders)
|
|
nozzle_slot_extruders = json.dumps(slot_extruders)
|
|
|
|
|
|
|
@@ -7348,7 +7430,7 @@ class PrintScheduler:
|
|
|
from backend.app.services.filament_requirements import extract_filament_requirements
|
|
from backend.app.services.filament_requirements import extract_filament_requirements
|
|
|
|
|
|
|
|
consumed = _consumed_mapping_entries(
|
|
consumed = _consumed_mapping_entries(
|
|
|
- ams_mapping, extract_filament_requirements(file_path, plate_id=item.plate_id or 1)
|
|
|
|
|
|
|
+ ams_mapping, extract_filament_requirements(file_path, plate_id=effective_plate_id)
|
|
|
)
|
|
)
|
|
|
if consumed and all(_is_external_tray(t) for t in consumed):
|
|
if consumed and all(_is_external_tray(t) for t in consumed):
|
|
|
effective_use_ams = False
|
|
effective_use_ams = False
|
|
@@ -7356,7 +7438,7 @@ class PrintScheduler:
|
|
|
"Queue item %s: every filament plate %s prints is on the external spool "
|
|
"Queue item %s: every filament plate %s prints is on the external spool "
|
|
|
"(mapping %s) — dispatching with use_ams=False (#3087)",
|
|
"(mapping %s) — dispatching with use_ams=False (#3087)",
|
|
|
item.id,
|
|
item.id,
|
|
|
- item.plate_id or 1,
|
|
|
|
|
|
|
+ effective_plate_id,
|
|
|
ams_mapping,
|
|
ams_mapping,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -7369,7 +7451,7 @@ class PrintScheduler:
|
|
|
started = printer_manager.start_print(
|
|
started = printer_manager.start_print(
|
|
|
item.printer_id,
|
|
item.printer_id,
|
|
|
remote_filename,
|
|
remote_filename,
|
|
|
- plate_id=item.plate_id or 1,
|
|
|
|
|
|
|
+ plate_id=effective_plate_id,
|
|
|
ams_mapping=ams_mapping,
|
|
ams_mapping=ams_mapping,
|
|
|
bed_levelling=item.bed_levelling,
|
|
bed_levelling=item.bed_levelling,
|
|
|
flow_cali=item.flow_cali,
|
|
flow_cali=item.flow_cali,
|